암호가 걸린 폴더 단위 압축 파일을 생성하는 기능이 필요하여, 안드로이드에서는 zip4j, iOS에서는 SSZipArchive 라이브러리를 사용하게 되었습니다.
api 레벨에서 zip 압축을 지원하기도 하는데, 폴더 단위 압축과 암호 설정이라는 조건을 더하면 구현도 복잡하고, 압축 파일에 대해서 잘 모르기 때문에 오픈소스의 힘을 빌리게 되었습니다. 그런데 오픈 소스 라이브러리들도 두가지 조건을 지원하는게 별로 없어서 어떤 면에서는 어쩔수 없이 두개 라이브러리를 선택했다고 보시면 됩니다. 물론 두개 라이브러리가 사용자도 많고, 프로젝트도 상대적으로 활성화되어 있는 상태입니다.
NoSuchMethodError
zip4j와 SSZipArchive를 이용하셔 개발은 순조롭게 진행이 되었습니다만, 테스트 과정에서 안드로이드 api 26 미만에서 아래와 같은 오류가 발생함이 확인되었습니다.
ava.lang.NoSuchMethodError: No virtual method toPath()Ljava/nio/file/Path; in class Ljava/io/File; or its super classes (declaration of 'java.io.File' appears in /system/framework/core-libart.jar)
at net.lingala.zip4j.util.FileUtils.getRelativeFileName(FileUtils.java:225)
at net.lingala.zip4j.tasks.AbstractAddFileToZipTask.cloneAndAdjustZipParameters(AbstractAddFileToZipTask.java:218)
at net.lingala.zip4j.tasks.AbstractAddFileToZipTask.addFilesToZip(AbstractAddFileToZipTask.java:71)
at net.lingala.zip4j.tasks.AddFolderToZipTask.executeTask(AddFolderToZipTask.java:28)
at net.lingala.zip4j.tasks.AddFolderToZipTask.executeTask(AddFolderToZipTask.java:17)
at net.lingala.zip4j.tasks.AsyncZipTask.performTaskWithErrorHandling(AsyncZipTask.java:51)
at net.lingala.zip4j.tasks.AsyncZipTask.execute(AsyncZipTask.java:45)
at net.lingala.zip4j.ZipFile.addFolder(ZipFile.java:391)
at net.lingala.zip4j.ZipFile.addFolder(ZipFile.java:366)
at net.lingala.zip4j.ZipFile.addFolder(ZipFile.java:332)
toPath() 메소드가 없다라는 오류입니다. toPath() 함수를 찾아보면 api 26부터 지원되는 함수인 때문에 당연히 발생하는 오류입니다
zip4j 프로젝트에 해당 오류(#)에 대해서 이슈 등록은 되어 있으나 개발자 피드백이 없는 해결되지 않은 상황입니다.
등록된 이슈 내용을 보면 2.11.1 버전에서는 문제가 없었다라는 내용이 있습니다. 그래서 2.11.2 버전 변경 사항을 따라가 보았습니다. 2.11.2 에 포함된 커밋중에 다음 보안 이슈 "vuln-fix: Partial Path Traversal Vulnerability"(#)를 수정하기 위해서 toPath() 함수를 사용한 것이이라서 2.11.3에서 문제가 해결될 것 같지도 않습니다.
나의 경우에는 앱 내부에서 사용되는 기능을 위해서 사용하는 것이기 때문에 해당 보안 이슈가 문제가 되지 않아서 2.11.1 버전을 사용해서 해결하는 것으로 결론을 지었습니다.
다음은 toPath() 이슈를 발생시키는 zip4j에서 addFolder() 함수를 사용하지 않으면서 폴더 단위 + 암호 적용된 zip 파일 생성을 위해서 테스트 구현했던 코드입니다.
final String password = "password";
ZipParameters parameters = new ZipParameters();
if (password.length() > 0) {
parameters.setEncryptFiles(true);
parameters.setEncryptionMethod(EncryptionMethod.AES);
parameters.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_256);
}
try {
FileOutputStream fos = new FileOutputStream(output);
Log.d(TAG, "output file : " + output);
ZipOutputStream zos = new ZipOutputStream(fos, password.toCharArray());
File fileToZip = new File(backup);
attachFileToZip(fileToZip, fileToZip.getName(), zos, parameters, false);
zos.close();
fos.close();
} catch (Exception e) {
e.printStackTrace();
return;
}
......
// 이 함수는 zip4j의 addFolder() 함수 내부에서 사용하는 함수를 가져와서 살짝 수정한 것입니다.
private ZipParameters cloneAndAdjustZipParameters(ZipParameters zipParameters, File fileToAdd) throws IOException {
ZipParameters clonedZipParameters = new ZipParameters(zipParameters);
if (fileToAdd.isDirectory()) {
clonedZipParameters.setEntrySize(0);
} else {
clonedZipParameters.setEntrySize(fileToAdd.length());
}
if (zipParameters.getLastModifiedFileTime() <= 0) {
clonedZipParameters.setLastModifiedFileTime(fileToAdd.lastModified());
}
clonedZipParameters.setWriteExtendedLocalFileHeader(false);
// net.lingala.zip4j.util.FileUtil.getRelativeFileName 내부에서 API 26부터 지원하는 함수를 사용하고 있다.
// 아래에 해당하는 코드는 별개로 처리해야한다.
// if (!Zip4jUtil.isStringNotNullAndNotEmpty(zipParameters.getFileNameInZip())) {
// String relativeFileName = getRelativeFileName(fileToAdd, zipParameters);
// clonedZipParameters.setFileNameInZip(relativeFileName);
// }
if (fileToAdd.isDirectory()) {
clonedZipParameters.setCompressionMethod(STORE);
clonedZipParameters.setEncryptionMethod(NONE);
clonedZipParameters.setEncryptFiles(false);
} else {
if (clonedZipParameters.isEncryptFiles() && clonedZipParameters.getEncryptionMethod() == ZIP_STANDARD) {
clonedZipParameters.setEntryCRC(computeFileCrc(fileToAdd, null/*progressMonitor*/));
}
if (fileToAdd.length() == 0) {
clonedZipParameters.setCompressionMethod(STORE);
}
}
return clonedZipParameters;
}
private void attachFileToZip(File fileToZip, String fileName, ZipOutputStream zipOut, ZipParameters parameters) throws IOException {
if (fileToZip.isHidden()) { return; }
ZipParameters zipParameters = cloneAndAdjustZipParameters(parameters, fileToZip);
if (fileToZip.isDirectory()) {
if (fileName.endsWith("/")) {
zipParameters.setFileNameInZip(fileName);
} else {
zipParameters.setFileNameInZip(fileName + "/");
}
zipOut.putNextEntry(zipParameters);
zipOut.closeEntry();
File[] children = fileToZip.listFiles();
for (File childFile : children) {
attachFileToZip(childFile, fileName + "/" + childFile.getName(), zipOut, parameters, false);
}
return;
}
zipParameters.setFileNameInZip(fileName);
zipOut.putNextEntry(zipParameters);
FileInputStream fis = new FileInputStream(fileToZip);
byte[] bytes = new byte[4096];
int length;
while ((length = fis.read(bytes)) >= 0) {
zipOut.write(bytes, 0, length);
}
fis.close();
zipOut.closeEntry();
}
위 코드는 테스트 목적으로 대략적으로 작성한 코드이기 때문에 일부 설정값등이 무시되기 떄문에 기대와 다르게 동작할 수 있으니 참고만 하시기 바랍니다. 개인적으로 나중에 다시 찾아보는 일을 줄이기 위해서 메모로 남깁니다.
댓글
댓글 쓰기