Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .maestro/flows/_home-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ appId: ${APP_ID}
---
- assertVisible: "Zip Operations"
- assertVisible: "Unzip Operations"
- assertVisible: "List & Selective Extract"
- scrollUntilVisible:
element:
text: "Password Protection"
direction: DOWN
- assertVisible: "Password Protection"
- scrollUntilVisible:
element:
text: "Progress Events"
direction: DOWN
- assertVisible: "Progress Events"
- scrollUntilVisible:
element:
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## [9.1.0] - 2026-07-25

### Added
- `listContents(source, charset?)` — inspect archive entries (path, sizes, directory/encrypted flags) without extracting (#365)
- `unzipFiles(source, target, entries, charset?)` — extract only selected entry paths; directory names include nested children (#365)
- `unzipFilesWithPassword(source, target, entries, password)` — selective extract for password-protected archives (#365)
- Android unit tests for selective-extract entry matching

## [9.0.2] - 2026-07-22

### Fixed
Expand Down
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ import {
zipWithPassword,
unzip,
unzipWithPassword,
unzipFiles,
unzipFilesWithPassword,
listContents,
unzipAssets,
subscribe,
isPasswordProtected,
Expand Down Expand Up @@ -129,6 +132,54 @@ unzipWithPassword(sourcePath, targetPath, 'password')
.catch((error) => console.error(error))
```

### `listContents(source: string, charset?: string): Promise<ZipEntry[]>`

List archive entries without extracting.

```ts
type ZipEntry = {
path: string
size: number // uncompressed size in bytes
compressedSize: number
isDirectory: boolean
isEncrypted: boolean
}
```

> The `charset` parameter is only supported on Android (default: `UTF-8`). On iOS it is ignored.

```js
listContents(sourcePath)
.then((entries) => {
entries.forEach((entry) => {
console.log(entry.path, entry.size, entry.isDirectory)
})
})
.catch((error) => console.error(error))
```

### `unzipFiles(source: string, target: string, entries: string[], charset?: string): Promise<string>`

Extract only the listed entry paths. Directory names match that entry and all nested children (e.g. `'docs'` extracts `docs/` and `docs/readme.md`).

> The `charset` parameter is only supported on Android (default: `UTF-8`). On iOS it is ignored.

```js
unzipFiles(sourcePath, targetPath, ['readme.md', 'docs'])
.then((path) => console.log(`selective unzip completed at ${path}`))
.catch((error) => console.error(error))
```

### `unzipFilesWithPassword(source: string, target: string, entries: string[], password: string): Promise<string>`

Selective extract for a password-protected archive.

```js
unzipFilesWithPassword(sourcePath, targetPath, ['secret.txt'], 'password')
.then((path) => console.log(`selective unzip completed at ${path}`))
.catch((error) => console.error(error))
```

### `unzipAssets(assetPath: string, target: string): Promise<string>`

Unzip a file from the Android `assets` folder. **Android only.**
Expand Down Expand Up @@ -189,6 +240,9 @@ useEffect(() => {
| `zipWithPassword` (files array) | ⚠️ | ✅ | iOS: only `STANDARD` encryption |
| `unzip` | ✅ | ✅ | Charset ignored on iOS |
| `unzipWithPassword` | ✅ | ✅ | — |
| `listContents` | ✅ | ✅ | Charset ignored on iOS |
| `unzipFiles` | ✅ | ✅ | Charset ignored on iOS |
| `unzipFilesWithPassword` | ✅ | ✅ | — |
| `unzipAssets` | ❌ | ✅ | Android only |
| `isPasswordProtected` | ✅ | ✅ | — |
| `getUncompressedSize` | ✅ | ✅ | Charset ignored on iOS |
Expand Down
3 changes: 3 additions & 0 deletions RNZipArchive.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Pod::Spec.new do |s|
s.source = { :git => 'https://github.com/mockingbot/react-native-zip-archive.git', :tag => "#{s.version}"}
s.platform = :ios, '15.5'
s.preserve_paths = '*.js'
s.pod_target_xcconfig = {
'HEADER_SEARCH_PATHS' => '$(inherited) "$(PODS_ROOT)/SSZipArchive/SSZipArchive/minizip"'
}

if defined?(install_modules_dependencies) != nil
install_modules_dependencies(s)
Expand Down
13 changes: 13 additions & 0 deletions __mocks__/react-native.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ const mockRNZipArchive = {
zipFilesWithPassword: jest.fn(() => Promise.resolve('/mock/path.zip')),
unzip: jest.fn(() => Promise.resolve('/mock/dest')),
unzipWithPassword: jest.fn(() => Promise.resolve('/mock/dest')),
unzipFiles: jest.fn(() => Promise.resolve('/mock/dest')),
unzipFilesWithPassword: jest.fn(() => Promise.resolve('/mock/dest')),
listContents: jest.fn(() =>
Promise.resolve([
{
path: 'hello.txt',
size: 12,
compressedSize: 10,
isDirectory: false,
isEncrypted: false,
},
])
),
unzipAssets: jest.fn(() => Promise.resolve('/mock/dest')),
isPasswordProtected: jest.fn(() => Promise.resolve(true)),
getUncompressedSize: jest.fn(() => Promise.resolve(1024)),
Expand Down
73 changes: 73 additions & 0 deletions __tests__/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ const {
zipWithPassword,
unzip,
unzipWithPassword,
unzipFiles,
unzipFilesWithPassword,
listContents,
unzipAssets,
isPasswordProtected,
getUncompressedSize,
Expand All @@ -25,6 +28,9 @@ describe('react-native-zip-archive API', () => {
expect(typeof zipWithPassword).toBe('function');
expect(typeof unzip).toBe('function');
expect(typeof unzipWithPassword).toBe('function');
expect(typeof unzipFiles).toBe('function');
expect(typeof unzipFilesWithPassword).toBe('function');
expect(typeof listContents).toBe('function');
expect(typeof unzipAssets).toBe('function');
expect(typeof isPasswordProtected).toBe('function');
expect(typeof getUncompressedSize).toBe('function');
Expand Down Expand Up @@ -107,6 +113,73 @@ describe('react-native-zip-archive API', () => {
});
});

describe('listContents', () => {
test('listContents with default charset', async () => {
const result = await listContents('/source.zip');
expect(result).toEqual([
{
path: 'hello.txt',
size: 12,
compressedSize: 10,
isDirectory: false,
isEncrypted: false,
},
]);
expect(mockRNZipArchive.listContents).toHaveBeenCalledWith('/source.zip', 'UTF-8');
});

test('listContents normalizes file:// paths', async () => {
await listContents('file:///source.zip', 'GBK');
expect(mockRNZipArchive.listContents).toHaveBeenCalledWith('/source.zip', 'GBK');
});
});

describe('unzipFiles', () => {
test('unzipFiles with default charset', async () => {
await unzipFiles('/source.zip', '/dest', ['a.txt', 'b.txt']);
expect(mockRNZipArchive.unzipFiles).toHaveBeenCalledWith(
'/source.zip',
'/dest',
['a.txt', 'b.txt'],
'UTF-8'
);
});

test('unzipFiles rejects empty entries', async () => {
await expect(unzipFiles('/source.zip', '/dest', [])).rejects.toThrow(
'unzipFiles requires a non-empty entries array'
);
});

test('unzipFiles normalizes file:// paths', async () => {
await unzipFiles('file:///source.zip', 'file:///dest', ['a.txt'], 'GBK');
expect(mockRNZipArchive.unzipFiles).toHaveBeenCalledWith(
'/source.zip',
'/dest',
['a.txt'],
'GBK'
);
});
});

describe('unzipFilesWithPassword', () => {
test('unzipFilesWithPassword calls native module', async () => {
await unzipFilesWithPassword('/source.zip', '/dest', ['a.txt'], 'secret');
expect(mockRNZipArchive.unzipFilesWithPassword).toHaveBeenCalledWith(
'/source.zip',
'/dest',
['a.txt'],
'secret'
);
});

test('unzipFilesWithPassword rejects empty entries', async () => {
await expect(
unzipFilesWithPassword('/source.zip', '/dest', [], 'secret')
).rejects.toThrow('unzipFilesWithPassword requires a non-empty entries array');
});
});

describe('isPasswordProtected', () => {
test('isPasswordProtected coerces to boolean', async () => {
mockRNZipArchive.isPasswordProtected.mockResolvedValueOnce(1);
Expand Down
3 changes: 3 additions & 0 deletions __tests__/module-integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const mockRNZipArchive = {
zipFilesWithPassword: jest.fn(() => Promise.resolve('/mock/path.zip')),
unzip: jest.fn(() => Promise.resolve('/mock/dest')),
unzipWithPassword: jest.fn(() => Promise.resolve('/mock/dest')),
unzipFiles: jest.fn(() => Promise.resolve('/mock/dest')),
unzipFilesWithPassword: jest.fn(() => Promise.resolve('/mock/dest')),
listContents: jest.fn(() => Promise.resolve([])),
unzipAssets: jest.fn(() => Promise.resolve('/mock/dest')),
isPasswordProtected: jest.fn(() => Promise.resolve(true)),
getUncompressedSize: jest.fn(() => Promise.resolve(1024)),
Expand Down
Loading
Loading