Repository navigation
Release Notes - v1.1.0
Release Date: October 5, 2025
Overview
Version 1.1.0 is a major security and stability update that hardens the native implementation on both iOS and Android platforms. This release adds comprehensive protection against concurrency issues, directory traversal attacks, and implements atomic extraction patterns.
Upgrade Command:
npm install react-native-unarchive@^1.1.0
# or
yarn upgrade react-native-unarchive@^1.1.0
# Then reinstall pods
cd ios && pod install🔒 Security Enhancements
ZIP-SLIP Protection (CVE Prevention)
- What it is: Protection against directory traversal attacks where malicious archives attempt to write files outside the extraction directory using paths like
../../etc/passwd - Implementation:
- iOS: Pre-extraction validation with
isSafePath:withinBaseURL:and post-extraction canonicalization checks - Android: Canonicalization validation with
isSafeEntryPath()before writing each entry
- iOS: Pre-extraction validation with
- Impact: Archives with malicious paths are rejected with
UNSAFE_PATHerror
Sandbox Path Enforcement
- What it is: Output paths are validated to be within app-controlled directories only
- iOS Allowed Paths:
- Documents:
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, ...) - Caches:
NSSearchPathForDirectoriesInDomains(NSCachesDirectory, ...) - Temp:
NSTemporaryDirectory()
- Documents:
- Android Allowed Paths:
- Files:
context.filesDir - Cache:
context.cacheDir - External:
context.getExternalFilesDir(null)
- Files:
- Impact: Prevents writes outside app sandbox; rejects with
UNARCHIVE_INVALID_PATHerror
Atomic Extraction Pattern
- What it is: Files are extracted to a temporary directory first, then atomically moved to the final destination
- Benefits:
- No partial extraction results visible to the app
- Automatic cleanup on failure
- Prevents race conditions from concurrent access to output directory
- Implementation:
- iOS:
replaceItemAtURL:withItemAtURL:for atomic swap - Android: Atomic move with rename-backup fallback
- iOS:
🚀 New Features
Cancellation Support
import { unarchive, cancelUnarchive } from 'react-native-unarchive';
// Start extraction
const promise = unarchive(archivePath, outputPath);
// Cancel if needed
await cancelUnarchive();
// Handle result or cancellation
try {
await promise;
} catch (error) {
if (error.code === 'UNARCHIVE_CANCELLED') {
console.log('User cancelled extraction');
}
}Directory Structure Preservation
-
New Field:
relativePathadded toFileInfotype -
Purpose: Maintains original archive directory structure
-
Example:
{ path: '/app/Documents/extracted/folder1/image.jpg', name: 'image.jpg', relativePath: 'folder1/image.jpg', // NEW! size: 12345 }
Comprehensive Error Codes
| Error Code | Description |
|---|---|
UNARCHIVE_BUSY |
Another extraction is in progress |
UNARCHIVE_INVALID_PATH |
Output path outside app sandbox |
UNSAFE_PATH |
Archive contains malicious paths |
UNARCHIVE_CANCELLED |
Extraction cancelled by user |
ATOMIC_REPLACE_ERROR |
Failed to finalize extraction |
FILE_NOT_FOUND |
Archive file doesn't exist |
DIRECTORY_ERROR |
Can't create extraction directory |
EXTRACTION_ERROR |
General extraction failure |
UNSUPPORTED_FORMAT |
Archive format not supported |
🔧 Technical Improvements
Concurrency Safety
iOS:
- Module-level
std::atomic_bool _activeExtractionprevents concurrent operations - Per-invocation
std::atomic_bool cbInvokedprevents double-callback resolveOnceandrejectOncehelper methods ensure single resolution
Android:
- Module-level
AtomicBoolean activeExtractionprevents concurrent operations - Per-invocation
AtomicBoolean cbInvokedprevents double-callback - Immediate
UNARCHIVE_BUSYrejection when already extracting
Thread Safety
iOS:
- Result payload construction moved to main thread via
dispatch_async(dispatch_get_main_queue()) - Immutable copies made before cross-thread dispatch
- Per-invocation
NSFileManagerinstead of shareddefaultManager
Android:
- Result payload conversion to
WritableNativeArray/MaponDispatchers.Main - POJOs collected on background thread, converted on main thread
- Single
FileOutputStreamper entry (prevents file handle leaks)
Debug Diagnostics
iOS (DEBUG builds only):
[Unarchive] Starting CBR extraction: archive.cbr
[Unarchive] Archive contains 125 entries
[Unarchive] Extracting to temp directory: /tmp/.unarchive_temp_...
[Unarchive] Extraction successful, enumerating files...
[Unarchive] Enumerated 125 files
[Unarchive] Performing atomic replacement to final location
[Unarchive] Extraction completed successfully with 125 files
Android (BuildConfig.DEBUG only):
[UnarchiveModule] Starting unarchive: /path/to/archive.cbz -> /path/to/output
[UnarchiveModule] Created temp directory: /path/to/.unarchive_temp_...
[UnarchiveModule] Opened archive successfully
[UnarchiveModule] Archive contains 125 items
[UnarchiveModule] Processing entry: folder/image001.jpg
[UnarchiveModule] Atomic move completed successfully
[UnarchiveModule] Unarchive completed successfully, extracted 125 files
🔄 Migration Guide
Path Changes (IMPORTANT)
Before (v1.0.1):
// This worked but is now rejected
const outputPath = '/sdcard/Download/archives';
await unarchive(archivePath, outputPath);After (v1.1.0):
import { DocumentDirectoryPath } from '@dr.pogodin/react-native-fs';
// Use app-scoped directories
const outputPath = `${DocumentDirectoryPath}/archives`;
await unarchive(archivePath, outputPath);Error Handling Changes
Before (v1.0.1):
try {
await unarchive(archivePath, outputPath);
} catch (error) {
console.error('Failed:', error.message);
}After (v1.1.0):
try {
await unarchive(archivePath, outputPath);
} catch (error) {
// Check specific error codes
if (error.code === 'UNARCHIVE_BUSY') {
console.log('Wait for current extraction to finish');
} else if (error.code === 'UNARCHIVE_INVALID_PATH') {
console.log('Use DocumentDirectoryPath or CachesDirectoryPath');
}
// ... handle other error codes
}Concurrency Changes
Before (v1.0.1):
// Multiple extractions could run simultaneously (undefined behavior)
const promise1 = unarchive(archive1, output1);
const promise2 = unarchive(archive2, output2);
await Promise.all([promise1, promise2]);After (v1.1.0):
// Only one extraction at a time; second will reject with UNARCHIVE_BUSY
try {
const promise1 = unarchive(archive1, output1);
const promise2 = unarchive(archive2, output2); // Throws UNARCHIVE_BUSY
await Promise.all([promise1, promise2]);
} catch (error) {
if (error.code === 'UNARCHIVE_BUSY') {
// Queue the second extraction
await promise1;
await unarchive(archive2, output2);
}
}⚡ Performance Improvements
- iOS: Reduced file I/O with per-invocation NSFileManager
- Android: Eliminated file handle leaks with single FileOutputStream per entry
- Both: Reduced memory usage with temp-dir extraction pattern
- Both: Minimized main thread blocking with proper thread dispatch
🐛 Bug Fixes
iOS
- Fixed race condition with concurrent extractions
- Fixed non-atomic directory replacement
- Fixed potential bridge crashes from background payload construction
- Fixed memory leaks in error paths
- Fixed missing cleanup of temp directories on cancellation
Android
- Fixed race condition with concurrent extractions
- Fixed file handle leaks from repeated stream creation
- Fixed crashes from WritableNativeArray construction off main thread
- Fixed non-atomic directory replacement
- Fixed missing cancellation implementation
🔬 Testing Recommendations
After upgrading to v1.1.0, test the following scenarios:
-
Path Validation:
// Should reject with UNARCHIVE_INVALID_PATH await unarchive(archivePath, '/invalid/path');
-
Concurrency:
// Second call should reject with UNARCHIVE_BUSY const p1 = unarchive(archive1, output1); const p2 = unarchive(archive2, output2);
-
Cancellation:
const promise = unarchive(largeArchive, output); setTimeout(() => cancelUnarchive(), 1000);
-
Directory Structure:
const result = await unarchive(archiveWithFolders, output); // Check that relativePath contains folder structure console.log(result.files.map(f => f.relativePath));
📦 What's Included in This Release
Changed Files
Native Code:
ios/Unarchive.mm- Complete rewrite with security hardeningios/Unarchive.h- Updated interfaceandroid/src/main/java/com/unarchive/UnarchiveModule.kt- Complete rewrite with security hardening
TypeScript:
src/NativeUnarchive.ts- AddedcancelUnarchive(),CancelResult,relativePathfieldsrc/index.tsx- ExportedcancelUnarchive()andCancelResulttype
Documentation:
README.md- Comprehensive updatesCHANGELOG.md- New file
Example App:
example/src/App.tsx- Major UI and functionality updates
🙏 Acknowledgments
This release represents a comprehensive security audit and hardening effort to ensure production-ready quality for the react-native-unarchive package.
📞 Support
For issues or questions:
- GitHub Issues: https://github.com/pushpender-singh-ap/react-native-unarchive/issues
- Documentation: See README.md
🔜 What's Next?
Planned for future releases:
- Comprehensive test suite for both platforms
- CI/CD integration for automated testing
- Progress callback support
- Additional archive format support
What's Changed
- fix: enforce sandbox validation, atomic replacement & main-thread result payload by @pushpender-singh-ap in #1
New Contributors
- @pushpender-singh-ap made their first contribution in #1
Full Changelog: 1.0.1...1.1.0