diff --git a/ios/flutter_7zip.podspec b/ios/flutter_7zip.podspec index a85338f..9c52919 100644 --- a/ios/flutter_7zip.podspec +++ b/ios/flutter_7zip.podspec @@ -23,6 +23,6 @@ A new Flutter FFI plugin project. s.platform = :ios, '12.0' # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++20' } s.swift_version = '5.0' end diff --git a/lib/flutter_7zip.dart b/lib/flutter_7zip.dart index 7bb707a..172b240 100644 --- a/lib/flutter_7zip.dart +++ b/lib/flutter_7zip.dart @@ -28,13 +28,42 @@ final Flutter7zipBindings _bindings = Flutter7zipBindings(_dylib); final _nativeFreeDataFunc = _dylib.lookup)>>('freeReadData'); +/// Timing statistics for a single extraction operation. +class FileExtractStat { + /// Index of the file in the archive. + final int fileIndex; + + /// Whether this entry was a directory. + final bool isDirectory; + + /// Elapsed milliseconds for this single file extraction. + final int elapsedMs; + + const FileExtractStat(this.fileIndex, this.isDirectory, this.elapsedMs); +} + +/// Wrapper around [SZArchive.extract] return value. +class ArchiveExtractResult { + /// Total elapsed milliseconds for the entire extraction. + final int totalElapsedMs; + + /// Number of files extracted. + final int totalFiles; + + const ArchiveExtractResult(this.totalElapsedMs, this.totalFiles); + + @override + String toString() => '${totalFiles}files/${totalElapsedMs}ms'; +} + class SZArchive { final Pointer _archive; + final _pointers = []; + final Stopwatch _stopwatch = Stopwatch(); + final List _fileStats = []; SZArchive._(this._archive); - final _pointers = []; - /// Dispose the archive and free all resources. void dispose() { _bindings.closeArchive(_archive); @@ -43,6 +72,18 @@ class SZArchive { } } + /// Total elapsed milliseconds spent extracting files from this archive. + int get elapsedMs => _stopwatch.elapsedMilliseconds; + + /// Per-file extraction statistics collected since opening. + List get fileStats => List.unmodifiable(_fileStats); + + /// Reset timing counters. + void resetTiming() { + _stopwatch.reset(); + _fileStats.clear(); + } + /// Get the number of files in the archive. int get numFiles => _bindings.getArchiveFileCount(_archive); @@ -107,9 +148,8 @@ class SZArchive { /// Extract the file at the given [index] to a file at the given [path]. void extractToFile(int index, String path) { var p = path.toNativeUtf8(); - var file = File(path); - if (!file.existsSync()) { - file.createSync(recursive: true); + if (!File(path).parent.existsSync()) { + File(path).parent.createSync(recursive: true); } final status = _bindings.extractArchiveToFile(_archive, index, p.cast()); malloc.free(p); @@ -118,6 +158,24 @@ class SZArchive { } } + /// Extract file by [index] to [outputDir], creating subdirs as needed. + /// Returns true if entry was a directory (already created). + /// Throws on error. + bool extractToDir(int index, String outputDir) { + _stopwatch.start(); + final before = _stopwatch.elapsedMilliseconds; + var p = outputDir.toNativeUtf8(); + final result = _bindings.extractFileToDir(_archive, index, p.cast()); + malloc.free(p); + if (result < 0 || result > 1) { + _stopwatch.stop(); + throw Exception('Failed to extract file index $index (code=$result)'); + } + final after = _stopwatch.elapsedMilliseconds; + _fileStats.add(FileExtractStat(index, result == 1, after - before)); + return result == 1; + } + /// Open an archive at the given [path]. static SZArchive open(String path) { var p = path.toNativeUtf8(); @@ -133,30 +191,29 @@ class SZArchive { } /// Extract the archive at the given [archivePath] to the given [outputPath]. - /// - /// The method is synchronous and will block the main thread. - static void extract(String archivePath, String outputPath) { + /// + /// Returns [ArchiveExtractResult] with total timing and file count. + static ArchiveExtractResult extract(String archivePath, String outputPath) { + final sw = Stopwatch()..start(); var archive = open(archivePath); - for (var i = 0; i < archive.numFiles; i++) { - var file = archive.getFile(i); - var outPath = outputPath + Platform.pathSeparator + file.name; - if (file.isDirectory) { - Directory(outPath).createSync(recursive: true); - } else { - archive.extractToFile(i, outPath); - } + var total = archive.numFiles; + for (var i = 0; i < total; i++) { + archive.extractToDir(i, outputPath); } archive.dispose(); + sw.stop(); + return ArchiveExtractResult(sw.elapsedMilliseconds, total); } /// Extract the archive at the given [archivePath] to the given [outputPath] with [isolatesCount] isolates. - /// + /// /// The method is asynchronous and will not block the main thread. - static Future extractIsolates( + static Future extractIsolates( String archivePath, String outputPath, int isolatesCount, ) async { + final sw = Stopwatch()..start(); var archive = open(archivePath); var total = archive.numFiles; var filesPerIsolate = total ~/ isolatesCount; @@ -172,6 +229,8 @@ class SZArchive { )); } await Future.wait(futures); + sw.stop(); + return ArchiveExtractResult(sw.elapsedMilliseconds, total); } static Future _extractIsolate( @@ -183,13 +242,7 @@ class SZArchive { return Isolate.run(() { var archive = open(archivePath); for (var i = start; i < end; i++) { - var file = archive.getFile(i); - var outPath = outputPath + Platform.pathSeparator + file.name; - if (file.isDirectory) { - Directory(outPath).createSync(recursive: true); - } else { - archive.extractToFile(i, outPath); - } + archive.extractToDir(i, outputPath); } archive.dispose(); }); diff --git a/lib/src/flutter_7zip_bindings_generated.dart b/lib/src/flutter_7zip_bindings_generated.dart index 37acc28..80ae5db 100644 --- a/lib/src/flutter_7zip_bindings_generated.dart +++ b/lib/src/flutter_7zip_bindings_generated.dart @@ -164,6 +164,25 @@ class Flutter7zipBindings { ffi.Pointer)>>('extractArchiveToFile'); late final _extractArchiveToFile = _extractArchiveToFilePtr.asFunction< int Function(ffi.Pointer, int, ffi.Pointer)>(); + + int extractFileToDir( + ffi.Pointer archive, + int index, + ffi.Pointer outputDir, + ) { + return _extractFileToDir( + archive, + index, + outputDir, + ); + } + + late final _extractFileToDirPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, ffi.Uint32, + ffi.Pointer)>>('extractFileToDir'); + late final _extractFileToDir = _extractFileToDirPtr.asFunction< + int Function(ffi.Pointer, int, ffi.Pointer)>(); } final class ArchiveFile extends ffi.Struct { diff --git a/macos/flutter_7zip.podspec b/macos/flutter_7zip.podspec index 12ec8b3..b821190 100644 --- a/macos/flutter_7zip.podspec +++ b/macos/flutter_7zip.podspec @@ -29,6 +29,6 @@ A new Flutter FFI plugin project. s.dependency 'FlutterMacOS' s.platform = :osx, '10.11' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++20' } s.swift_version = '5.0' end diff --git a/src/flutter_7zip.cpp b/src/flutter_7zip.cpp index 1929960..4a430ac 100644 --- a/src/flutter_7zip.cpp +++ b/src/flutter_7zip.cpp @@ -4,6 +4,9 @@ #include "7zip/C/7zFile.h" #include #include +#include +#include +#include void *_AllocImp(ISzAllocPtr p, size_t size) { return malloc(size); @@ -23,6 +26,9 @@ class Archive { CSzArEx db; ISzAlloc allocImp = AllocImp; ISzAlloc allocTempImp = AllocImp; + uint32_t lastBlockIndex = 0xFFFFFFFF; + Byte* cachedBuffer = nullptr; + size_t cachedBufferSize = 0; public: ArchiveStatus status = kArchiveOK; @@ -39,7 +45,8 @@ class Archive { lookStream.bufSize = 16 * 1024; LookToRead2_CreateVTable(&lookStream, False); SzArEx_Init(&db); - if (const auto res = SzArEx_Open(&db, &lookStream.vt, &allocImp, &allocTempImp); res != SZ_OK) { + const SRes openRes = SzArEx_Open(&db, &lookStream.vt, &allocImp, &allocTempImp); + if (openRes != SZ_OK) { status = kArchiveOpenError; File_Close(&archiveStream.file); } @@ -49,6 +56,7 @@ class Archive { SzArEx_Free(&db, &allocImp); File_Close(&archiveStream.file); delete [] lookStream.buf; + _FreeImp(&allocImp, cachedBuffer); } uint32_t numFiles() const { @@ -76,67 +84,53 @@ class Archive { } else { archiveFile.crc32 = 0; } - - if (db.CTime.Defs != nullptr) { - if (db.CTime.Defs[index]) { - auto [Low, High] = db.CTime.Vals[index]; - archiveFile.cTime = Low + (static_cast(High) << 32); - } else { - archiveFile.cTime = 0; - } - } else { - archiveFile.cTime = 0; + + archiveFile.cTime = 0; + if (db.CTime.Defs != nullptr && db.CTime.Defs[index]) { + const UInt64 Low = db.CTime.Vals[index].Low; + const UInt64 High = db.CTime.Vals[index].High; + archiveFile.cTime = Low + (High << 32); } - if (db.MTime.Defs != nullptr) { - if (db.MTime.Defs[index]) { - auto [Low, High] = db.MTime.Vals[index]; - archiveFile.mTime = Low + (static_cast(High) << 32); - } else { - archiveFile.mTime = 0; - } - } else { - archiveFile.mTime = 0; + archiveFile.mTime = 0; + if (db.MTime.Defs != nullptr && db.MTime.Defs[index]) { + const UInt64 Low = db.MTime.Vals[index].Low; + const UInt64 High = db.MTime.Vals[index].High; + archiveFile.mTime = Low + (High << 32); } return archiveFile; } - unsigned char* readFile(const uint32_t index) const { + unsigned char* readFile(const uint32_t index) { const ArchiveFile archiveFile = getFileByIndex(index); if (archiveFile.is_dir) { return nullptr; } auto* buffer = new unsigned char[archiveFile.size]; size_t read = 0; - uint32_t blockIndex; - Byte* outBuffer = nullptr; - size_t outBufferSize; size_t offset; size_t outSizeProcessed; while (read < archiveFile.size) { - if (const auto res = SzArEx_Extract(&db, &lookStream.vt, index, &blockIndex, &outBuffer, &outBufferSize, &offset, &outSizeProcessed, &allocImp, &allocTempImp); res != SZ_OK) { + const SRes res = SzArEx_Extract(&db, &lookStream.vt, index, &lastBlockIndex, &cachedBuffer, &cachedBufferSize, &offset, &outSizeProcessed, &allocImp, &allocTempImp); + if (res != SZ_OK) { delete[] buffer; return nullptr; } for (auto i = offset; i < outSizeProcessed + offset; i++) { - buffer[read] = outBuffer[i]; + buffer[read] = cachedBuffer[i]; read++; } - _FreeImp(&allocImp, outBuffer); } return buffer; } - ArchiveStatus extractFileToPath(const uint32_t index, const char* path) const { + ArchiveStatus extractFileToPath(const uint32_t index, const char* path) { const ArchiveFile archiveFile = getFileByIndex(index); if (archiveFile.is_dir) { return kArchiveReadError; } size_t read = 0; - uint32_t blockIndex; - Byte* outBuffer = nullptr; - size_t outBufferSize; size_t offset; size_t outSizeProcessed; std::ofstream outFile; @@ -148,17 +142,75 @@ class Archive { return kArchiveOpenError; } while (read < archiveFile.size) { - if (const auto res = SzArEx_Extract(&db, &lookStream.vt, index, &blockIndex, &outBuffer, &outBufferSize, &offset, &outSizeProcessed, &allocImp, &allocTempImp); res != SZ_OK) { + const SRes res = SzArEx_Extract(&db, &lookStream.vt, index, &lastBlockIndex, &cachedBuffer, &cachedBufferSize, &offset, &outSizeProcessed, &allocImp, &allocTempImp); + if (res != SZ_OK) { outFile.close(); return ArchiveStatus::kArchiveReadError; } - outFile.write(reinterpret_cast(outBuffer+offset), outSizeProcessed); + outFile.write(reinterpret_cast(cachedBuffer+offset), outSizeProcessed); read += outSizeProcessed; - _FreeImp(&allocImp, outBuffer); } outFile.close(); return kArchiveOK; } + + int extractToDir(const uint32_t index, const char* outputDir) { + const ArchiveFile file = getFileByIndex(index); + + std::string outPath = std::string(outputDir) + "/"; + const uint16_t* src = file.name; + while (*src) { + if (*src < 0x80) { + outPath += static_cast(*src); + } else if (*src < 0x800) { + outPath += static_cast(0xC0 | (*src >> 6)); + outPath += static_cast(0x80 | (*src & 0x3F)); + } else { + outPath += static_cast(0xE0 | (*src >> 12)); + outPath += static_cast(0x80 | ((*src >> 6) & 0x3F)); + outPath += static_cast(0x80 | (*src & 0x3F)); + } + src++; + } + delete[] file.name; + + if (file.is_dir) { + size_t pos = 0; + while ((pos = outPath.find_first_of('/', pos)) != std::string::npos) { + std::string dir = outPath.substr(0, pos); + if (!dir.empty()) mkdir(dir.c_str(), 0755); + pos++; + } + mkdir(outPath.c_str(), 0755); + return 1; + } + + size_t pos = 0; + while ((pos = outPath.find_first_of('/', pos)) != std::string::npos) { + std::string dir = outPath.substr(0, pos); + mkdir(dir.c_str(), 0755); + pos++; + } + + size_t read = 0; + size_t offset; + size_t outSizeProcessed; + std::ofstream outFile; + outFile.open(outPath, std::ios::binary | std::ios::out); + if (!outFile.is_open()) return 2; + + while (read < file.size) { + const SRes res = SzArEx_Extract(&db, &lookStream.vt, index, &lastBlockIndex, &cachedBuffer, &cachedBufferSize, &offset, &outSizeProcessed, &allocImp, &allocTempImp); + if (res != SZ_OK) { + outFile.close(); + return 3; + } + outFile.write(reinterpret_cast(cachedBuffer+offset), outSizeProcessed); + read += outSizeProcessed; + } + outFile.close(); + return 0; + } }; FFI_PLUGIN_EXPORT void freeArchiveFile(const ArchiveFile archive) { @@ -200,3 +252,8 @@ FFI_PLUGIN_EXPORT ArchiveStatus extractArchiveToFile(void* archive, uint32_t ind const auto a = static_cast(archive); return a->extractFileToPath(index, path); } + +FFI_PLUGIN_EXPORT int extractFileToDir(void* archive, uint32_t index, const char* outputDir) { + const auto a = static_cast(archive); + return a->extractToDir(index, outputDir); +} \ No newline at end of file diff --git a/src/flutter_7zip.h b/src/flutter_7zip.h index aaacb7d..8c4e5d2 100644 --- a/src/flutter_7zip.h +++ b/src/flutter_7zip.h @@ -45,6 +45,10 @@ extern "C" { FFI_PLUGIN_EXPORT void freeReadData(void* p); FFI_PLUGIN_EXPORT ArchiveStatus extractArchiveToFile(void* archive, uint32_t index, const char* path); + + // Extract file by index to dir, creating subdirectories as needed. + // Returns 0 on success, 1 if entry is a directory (already created), >1 on error. + FFI_PLUGIN_EXPORT int extractFileToDir(void* archive, uint32_t index, const char* outputDir); #ifdef __cplusplus }; #endif