365 lines
14 KiB
Objective-C
365 lines
14 KiB
Objective-C
#import "ZipUtils.h"
|
|
#import <zlib.h>
|
|
#import "zeus-Swift.h"
|
|
|
|
// Minizip-compatible local file header constants
|
|
#define ZIP_LOCAL_HEADER_SIGNATURE 0x04034b50
|
|
#define ZIP_CENTRAL_DIR_SIGNATURE 0x02014b50
|
|
#define ZIP_END_CENTRAL_DIR_SIGNATURE 0x06054b50
|
|
#define ZIP_VERSION_NEEDED 20
|
|
#define ZIP_COMPRESSION_DEFLATE 8
|
|
#define ZIP_COMPRESSION_STORE 0
|
|
|
|
@implementation ZipUtils
|
|
|
|
RCT_EXPORT_MODULE();
|
|
|
|
+ (BOOL)requiresMainQueueSetup {
|
|
return NO;
|
|
}
|
|
|
|
#pragma mark - Zip
|
|
|
|
RCT_EXPORT_METHOD(zipFolder:(NSString *)sourceDirPath
|
|
destPath:(NSString *)destZipPath
|
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
{
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
BOOL isDir;
|
|
if (![fm fileExistsAtPath:sourceDirPath isDirectory:&isDir] || !isDir) {
|
|
reject(@"ERR_ZIP", [NSString stringWithFormat:@"Source directory does not exist: %@", sourceDirPath], nil);
|
|
return;
|
|
}
|
|
|
|
NSError *error;
|
|
NSArray<NSString *> *files = [fm contentsOfDirectoryAtPath:sourceDirPath error:&error];
|
|
if (error) {
|
|
reject(@"ERR_ZIP", error.localizedDescription, error);
|
|
return;
|
|
}
|
|
|
|
NSMutableData *zipData = [NSMutableData data];
|
|
NSMutableArray *centralDirEntries = [NSMutableArray array];
|
|
uint32_t centralDirOffset = 0;
|
|
|
|
for (NSString *fileName in files) {
|
|
NSString *filePath = [sourceDirPath stringByAppendingPathComponent:fileName];
|
|
BOOL fileIsDir;
|
|
if (![fm fileExistsAtPath:filePath isDirectory:&fileIsDir] || fileIsDir) continue;
|
|
|
|
NSData *fileData = [NSData dataWithContentsOfFile:filePath];
|
|
if (!fileData) continue;
|
|
|
|
NSData *compressedData = [self deflateData:fileData];
|
|
BOOL useStore = (compressedData == nil || compressedData.length >= fileData.length);
|
|
NSData *dataToWrite = useStore ? fileData : compressedData;
|
|
uint16_t method = useStore ? ZIP_COMPRESSION_STORE : ZIP_COMPRESSION_DEFLATE;
|
|
|
|
uint32_t crc = (uint32_t)crc32(0L, fileData.bytes, (uInt)fileData.length);
|
|
NSData *fileNameData = [fileName dataUsingEncoding:NSUTF8StringEncoding];
|
|
|
|
uint32_t localHeaderOffset = (uint32_t)zipData.length;
|
|
|
|
// Local file header
|
|
uint32_t sig = ZIP_LOCAL_HEADER_SIGNATURE;
|
|
uint16_t versionNeeded = ZIP_VERSION_NEEDED;
|
|
uint16_t flags = 0;
|
|
uint16_t nameLen = (uint16_t)fileNameData.length;
|
|
uint16_t extraLen = 0;
|
|
uint32_t compSize = (uint32_t)dataToWrite.length;
|
|
uint32_t uncompSize = (uint32_t)fileData.length;
|
|
|
|
[zipData appendBytes:&sig length:4];
|
|
[zipData appendBytes:&versionNeeded length:2];
|
|
[zipData appendBytes:&flags length:2];
|
|
[zipData appendBytes:&method length:2];
|
|
uint16_t modTime = 0, modDate = 0;
|
|
[zipData appendBytes:&modTime length:2];
|
|
[zipData appendBytes:&modDate length:2];
|
|
[zipData appendBytes:&crc length:4];
|
|
[zipData appendBytes:&compSize length:4];
|
|
[zipData appendBytes:&uncompSize length:4];
|
|
[zipData appendBytes:&nameLen length:2];
|
|
[zipData appendBytes:&extraLen length:2];
|
|
[zipData appendData:fileNameData];
|
|
[zipData appendData:dataToWrite];
|
|
|
|
// Build central directory entry
|
|
NSMutableData *cdEntry = [NSMutableData data];
|
|
uint32_t cdSig = ZIP_CENTRAL_DIR_SIGNATURE;
|
|
uint16_t versionMade = ZIP_VERSION_NEEDED;
|
|
uint16_t diskNum = 0;
|
|
uint16_t intAttr = 0;
|
|
uint32_t extAttr = 0;
|
|
|
|
[cdEntry appendBytes:&cdSig length:4];
|
|
[cdEntry appendBytes:&versionMade length:2];
|
|
[cdEntry appendBytes:&versionNeeded length:2];
|
|
[cdEntry appendBytes:&flags length:2];
|
|
[cdEntry appendBytes:&method length:2];
|
|
[cdEntry appendBytes:&modTime length:2];
|
|
[cdEntry appendBytes:&modDate length:2];
|
|
[cdEntry appendBytes:&crc length:4];
|
|
[cdEntry appendBytes:&compSize length:4];
|
|
[cdEntry appendBytes:&uncompSize length:4];
|
|
[cdEntry appendBytes:&nameLen length:2];
|
|
[cdEntry appendBytes:&extraLen length:2];
|
|
uint16_t commentLen = 0;
|
|
[cdEntry appendBytes:&commentLen length:2];
|
|
[cdEntry appendBytes:&diskNum length:2];
|
|
[cdEntry appendBytes:&intAttr length:2];
|
|
[cdEntry appendBytes:&extAttr length:4];
|
|
[cdEntry appendBytes:&localHeaderOffset length:4];
|
|
[cdEntry appendData:fileNameData];
|
|
|
|
[centralDirEntries addObject:cdEntry];
|
|
}
|
|
|
|
centralDirOffset = (uint32_t)zipData.length;
|
|
uint32_t centralDirSize = 0;
|
|
for (NSData *entry in centralDirEntries) {
|
|
[zipData appendData:entry];
|
|
centralDirSize += (uint32_t)entry.length;
|
|
}
|
|
|
|
// End of central directory
|
|
uint32_t eocdSig = ZIP_END_CENTRAL_DIR_SIGNATURE;
|
|
uint16_t diskNum = 0;
|
|
uint16_t numEntries = (uint16_t)centralDirEntries.count;
|
|
uint16_t commentLen = 0;
|
|
|
|
[zipData appendBytes:&eocdSig length:4];
|
|
[zipData appendBytes:&diskNum length:2];
|
|
[zipData appendBytes:&diskNum length:2];
|
|
[zipData appendBytes:&numEntries length:2];
|
|
[zipData appendBytes:&numEntries length:2];
|
|
[zipData appendBytes:¢ralDirSize length:4];
|
|
[zipData appendBytes:¢ralDirOffset length:4];
|
|
[zipData appendBytes:&commentLen length:2];
|
|
|
|
BOOL success = [zipData writeToFile:destZipPath atomically:YES];
|
|
if (success) {
|
|
resolve(nil);
|
|
} else {
|
|
reject(@"ERR_ZIP", @"Failed to write zip file", nil);
|
|
}
|
|
});
|
|
}
|
|
|
|
- (NSData *)deflateData:(NSData *)data {
|
|
if (data.length == 0) return nil;
|
|
|
|
z_stream stream;
|
|
memset(&stream, 0, sizeof(stream));
|
|
// -MAX_WBITS for raw deflate (no zlib/gzip header)
|
|
if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
|
|
return nil;
|
|
}
|
|
|
|
stream.next_in = (Bytef *)data.bytes;
|
|
stream.avail_in = (uInt)data.length;
|
|
|
|
NSMutableData *compressed = [NSMutableData dataWithLength:deflateBound(&stream, (uLong)data.length)];
|
|
stream.next_out = (Bytef *)compressed.mutableBytes;
|
|
stream.avail_out = (uInt)compressed.length;
|
|
|
|
int status = deflate(&stream, Z_FINISH);
|
|
deflateEnd(&stream);
|
|
|
|
if (status != Z_STREAM_END) return nil;
|
|
|
|
compressed.length = stream.total_out;
|
|
return compressed;
|
|
}
|
|
|
|
#pragma mark - Unzip
|
|
|
|
RCT_EXPORT_METHOD(unzipFile:(NSString *)zipPath
|
|
destPath:(NSString *)destDirPath
|
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
{
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
NSFileManager *fm = [NSFileManager defaultManager];
|
|
if (![fm fileExistsAtPath:zipPath]) {
|
|
reject(@"ERR_UNZIP", [NSString stringWithFormat:@"Zip file does not exist: %@", zipPath], nil);
|
|
return;
|
|
}
|
|
|
|
NSData *zipData = [NSData dataWithContentsOfFile:zipPath];
|
|
if (!zipData || zipData.length < 22) {
|
|
reject(@"ERR_UNZIP", @"Invalid or empty zip file", nil);
|
|
return;
|
|
}
|
|
|
|
if (![fm fileExistsAtPath:destDirPath]) {
|
|
NSError *error;
|
|
[fm createDirectoryAtPath:destDirPath withIntermediateDirectories:YES attributes:nil error:&error];
|
|
if (error) {
|
|
reject(@"ERR_UNZIP", error.localizedDescription, error);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const uint8_t *bytes = zipData.bytes;
|
|
NSUInteger length = zipData.length;
|
|
NSUInteger offset = 0;
|
|
|
|
while (offset + 30 <= length) {
|
|
uint32_t sig = *(uint32_t *)(bytes + offset);
|
|
if (sig != ZIP_LOCAL_HEADER_SIGNATURE) break;
|
|
|
|
uint16_t method = *(uint16_t *)(bytes + offset + 8);
|
|
uint32_t crc = *(uint32_t *)(bytes + offset + 14);
|
|
uint32_t compSize = *(uint32_t *)(bytes + offset + 18);
|
|
uint32_t uncompSize = *(uint32_t *)(bytes + offset + 22);
|
|
uint16_t nameLen = *(uint16_t *)(bytes + offset + 26);
|
|
uint16_t extraLen = *(uint16_t *)(bytes + offset + 28);
|
|
|
|
if (offset + 30 + nameLen + extraLen + compSize > length) {
|
|
reject(@"ERR_UNZIP", @"Truncated zip file", nil);
|
|
return;
|
|
}
|
|
|
|
NSString *entryName = [[NSString alloc] initWithBytes:(bytes + offset + 30) length:nameLen encoding:NSUTF8StringEncoding];
|
|
NSUInteger dataOffset = offset + 30 + nameLen + extraLen;
|
|
|
|
offset = dataOffset + compSize;
|
|
|
|
if (!entryName || [entryName hasSuffix:@"/"] || [entryName containsString:@".."]) continue;
|
|
|
|
// Strip path components — only use the filename
|
|
NSString *safeName = [entryName lastPathComponent];
|
|
NSString *destFilePath = [destDirPath stringByAppendingPathComponent:safeName];
|
|
|
|
NSData *fileData;
|
|
if (method == ZIP_COMPRESSION_STORE) {
|
|
fileData = [NSData dataWithBytes:(bytes + dataOffset) length:compSize];
|
|
} else if (method == ZIP_COMPRESSION_DEFLATE) {
|
|
fileData = [self inflateData:[NSData dataWithBytesNoCopy:(void *)(bytes + dataOffset) length:compSize freeWhenDone:NO]
|
|
expectedSize:uncompSize];
|
|
if (!fileData) {
|
|
reject(@"ERR_UNZIP", [NSString stringWithFormat:@"Failed to decompress: %@", entryName], nil);
|
|
return;
|
|
}
|
|
} else {
|
|
reject(@"ERR_UNZIP", [NSString stringWithFormat:@"Unsupported compression method %d for: %@", method, entryName], nil);
|
|
return;
|
|
}
|
|
|
|
// Verify CRC
|
|
uint32_t actualCrc = (uint32_t)crc32(0L, fileData.bytes, (uInt)fileData.length);
|
|
if (actualCrc != crc && crc != 0) {
|
|
reject(@"ERR_UNZIP", [NSString stringWithFormat:@"CRC mismatch for: %@", entryName], nil);
|
|
return;
|
|
}
|
|
|
|
[fileData writeToFile:destFilePath atomically:YES];
|
|
}
|
|
|
|
resolve(nil);
|
|
});
|
|
}
|
|
|
|
- (NSData *)inflateData:(NSData *)data expectedSize:(uint32_t)expectedSize {
|
|
if (data.length == 0) return [NSData data];
|
|
|
|
z_stream stream;
|
|
memset(&stream, 0, sizeof(stream));
|
|
// -MAX_WBITS for raw inflate (no zlib/gzip header)
|
|
if (inflateInit2(&stream, -MAX_WBITS) != Z_OK) {
|
|
return nil;
|
|
}
|
|
|
|
stream.next_in = (Bytef *)data.bytes;
|
|
stream.avail_in = (uInt)data.length;
|
|
|
|
NSMutableData *decompressed = [NSMutableData dataWithLength:expectedSize > 0 ? expectedSize : data.length * 4];
|
|
stream.next_out = (Bytef *)decompressed.mutableBytes;
|
|
stream.avail_out = (uInt)decompressed.length;
|
|
|
|
int status = inflate(&stream, Z_FINISH);
|
|
|
|
if (status != Z_STREAM_END && status != Z_OK) {
|
|
// Try with more buffer
|
|
if (status == Z_BUF_ERROR) {
|
|
decompressed.length = decompressed.length * 2;
|
|
stream.next_out = (Bytef *)decompressed.mutableBytes + stream.total_out;
|
|
stream.avail_out = (uInt)(decompressed.length - stream.total_out);
|
|
status = inflate(&stream, Z_FINISH);
|
|
}
|
|
if (status != Z_STREAM_END && status != Z_OK) {
|
|
inflateEnd(&stream);
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
decompressed.length = stream.total_out;
|
|
inflateEnd(&stream);
|
|
return decompressed;
|
|
}
|
|
|
|
#pragma mark - Encrypt
|
|
|
|
RCT_EXPORT_METHOD(encryptFile:(NSString *)inputPath
|
|
outputPath:(NSString *)outputPath
|
|
passphrase:(NSString *)passphrase
|
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
{
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
NSData *plaintext = [NSData dataWithContentsOfFile:inputPath];
|
|
if (!plaintext) {
|
|
reject(@"ERR_ENCRYPT", [NSString stringWithFormat:@"Input file does not exist: %@", inputPath], nil);
|
|
return;
|
|
}
|
|
|
|
NSError *error = nil;
|
|
NSData *output = [CryptoHelper encryptData:plaintext passphrase:passphrase error:&error];
|
|
if (!output) {
|
|
reject(@"ERR_ENCRYPT", error.localizedDescription ?: @"Encryption failed", error);
|
|
return;
|
|
}
|
|
|
|
if ([output writeToFile:outputPath atomically:YES]) {
|
|
resolve(nil);
|
|
} else {
|
|
reject(@"ERR_ENCRYPT", @"Failed to write encrypted file", nil);
|
|
}
|
|
});
|
|
}
|
|
|
|
#pragma mark - Decrypt
|
|
|
|
RCT_EXPORT_METHOD(decryptFile:(NSString *)inputPath
|
|
outputPath:(NSString *)outputPath
|
|
passphrase:(NSString *)passphrase
|
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
{
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
NSData *fileData = [NSData dataWithContentsOfFile:inputPath];
|
|
if (!fileData) {
|
|
reject(@"ERR_DECRYPT", [NSString stringWithFormat:@"Input file does not exist: %@", inputPath], nil);
|
|
return;
|
|
}
|
|
|
|
NSError *error = nil;
|
|
NSData *plaintext = [CryptoHelper decryptData:fileData passphrase:passphrase error:&error];
|
|
if (!plaintext) {
|
|
reject(@"ERR_DECRYPT", error.localizedDescription ?: @"Decryption failed. Incorrect seed or corrupted file.", error);
|
|
return;
|
|
}
|
|
|
|
if ([plaintext writeToFile:outputPath atomically:YES]) {
|
|
resolve(nil);
|
|
} else {
|
|
reject(@"ERR_DECRYPT", @"Failed to write decrypted file", nil);
|
|
}
|
|
});
|
|
}
|
|
|
|
@end
|