diff --git a/.gitignore b/.gitignore index 51d84a0..262f4c9 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,9 @@ Cargo.lock x86_64-unknown-linux-gnu *.wasm /npm + +# Test output files +__test__/*.tar +__test__/test-* +__test__/temp* +__test__/mixed-* diff --git a/README.md b/README.md index 1ba2ffc..664798c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ ## Usage +### Reading Archives + ```ts export class Entries { [Symbol.iterator](): Iterator @@ -35,6 +37,43 @@ export class Archive { } ``` +### Creating Archives + +```ts +export class Builder { + /** Create a new builder which will write to the specified output. */ + constructor(output?: string) + /** Append a file from disk to this archive. */ + appendFile(name: string, src: string): void + /** Append a directory and all of its contents to this archive. */ + appendDirAll(name: string, src: string): void + /** Append raw data to this archive with the specified name. */ + appendData(name: string, data: Uint8Array): void + /** Finalize the archive and return the resulting data. */ + finish(): Array | null +} +``` + +### Creating Archives Example + +```ts +import { Builder } from '@napi-rs/tar' + +// Create archive in memory +const builder = new Builder() +builder.appendData('hello.txt', Buffer.from('Hello, world!')) +builder.appendFile('package.json', './package.json') +builder.appendDirAll('src', './src') + +const archiveData = builder.finish() // Returns Uint8Array +// archiveData can be written to disk or used directly + +// Create archive to file +const fileBuilder = new Builder('./output.tar') +fileBuilder.appendData('readme.txt', Buffer.from('Archive contents')) +fileBuilder.finish() // Returns null, data written to ./output.tar +``` + ## Extract Single File You can extract a specific file from a tar archive without extracting the entire archive. This is useful for inspecting Docker OCI images or extracting specific configuration files: diff --git a/__test__/index.spec.ts b/__test__/index.spec.ts index 3fae5fc..b192e52 100644 --- a/__test__/index.spec.ts +++ b/__test__/index.spec.ts @@ -1,10 +1,11 @@ import { readFile } from 'node:fs/promises' import { join } from 'node:path' import { fileURLToPath } from 'node:url' +import { writeFileSync, unlinkSync, mkdirSync, rmSync } from 'node:fs' import test from 'ava' -import { Archive } from '../index' +import { Archive, Builder } from '../index' const __dirname = join(fileURLToPath(import.meta.url), '..') @@ -170,3 +171,355 @@ test('Docker OCI use case - extract specific file like index.json', (t) => { const nonExistentContent = extractFile(archivePath, 'non-existent.json') t.is(nonExistentContent, null, 'Should return null for non-existent files') }) + +// ===================================== +// Builder API Tests +// ===================================== + +test('Builder - should create archive with appendData', (t) => { + const builder = new Builder() + + // Add some data to the archive + builder.appendData('hello.txt', Buffer.from('Hello, world!')) + builder.appendData('test.txt', Buffer.from('Test content')) + + // Finish and get the archive data + const archiveData = builder.finish() + t.not(archiveData, null, 'Should return archive data') + t.true(archiveData instanceof Array, 'Should return an array of bytes') + t.true(archiveData!.length > 0, 'Archive data should not be empty') + + // Verify the archive by reading it back + const archive = new Archive(Buffer.from(archiveData!)) + const fileNames: string[] = [] + + for (const entry of archive.entries()) { + const path = entry.path() + if (path) { + fileNames.push(path) + } + } + + t.true(fileNames.includes('hello.txt'), 'Should contain hello.txt') + t.true(fileNames.includes('test.txt'), 'Should contain test.txt') +}) + +test('Builder - should create archive with file output', (t) => { + const outputPath = join(__dirname, 'test-output.tar') + const builder = new Builder(outputPath) + + // Add some data + builder.appendData('file1.txt', Buffer.from('Content 1')) + builder.appendData('file2.txt', Buffer.from('Content 2')) + + // Finish (should write to file) + const result = builder.finish() + t.is(result, null, 'Should return null for file output') + + // Verify the file was created by reading it + const archive = new Archive(outputPath) + const fileNames: string[] = [] + + for (const entry of archive.entries()) { + const path = entry.path() + if (path) { + fileNames.push(path) + } + } + + t.is(fileNames.length, 2, 'Should contain 2 files') + t.true(fileNames.includes('file1.txt'), 'Should contain file1.txt') + t.true(fileNames.includes('file2.txt'), 'Should contain file2.txt') +}) + +test('Builder - should append files from disk', (t) => { + if (process.env.NAPI_RS_FORCE_WASI) { + t.pass('Skipping append files test on WASI') + return + } + // Create temp files to add to archive + const tempFile1 = join(__dirname, 'temp1.txt') + const tempFile2 = join(__dirname, 'temp2.txt') + + // Write test files + writeFileSync(tempFile1, 'Temp file 1 content') + writeFileSync(tempFile2, 'Temp file 2 content') + + const builder = new Builder() + + // Add files to archive + builder.appendFile('archived_temp1.txt', tempFile1) + builder.appendFile('archived_temp2.txt', tempFile2) + + // Finish and verify + const archiveData = builder.finish() + t.not(archiveData, null, 'Should return archive data') + + const archive = new Archive(Buffer.from(archiveData!)) + const fileContents = new Map() + + for (const entry of archive.entries()) { + const path = entry.path() + if (path) { + const content = entry.asBytes().toString('utf-8') + fileContents.set(path, content) + } + } + + t.is(fileContents.get('archived_temp1.txt'), 'Temp file 1 content', 'Should have correct content for temp1') + t.is(fileContents.get('archived_temp2.txt'), 'Temp file 2 content', 'Should have correct content for temp2') + + // Clean up temp files + unlinkSync(tempFile1) + unlinkSync(tempFile2) +}) + +test('Builder - should append directories', (t) => { + if (process.env.NAPI_RS_FORCE_WASI) { + t.pass('Skipping append directories test on WASI') + return + } + // Create a test directory structure + const testDir = join(__dirname, 'test-dir') + const subDir = join(testDir, 'subdir') + + mkdirSync(testDir, { recursive: true }) + mkdirSync(subDir, { recursive: true }) + writeFileSync(join(testDir, 'file1.txt'), 'Root file content') + writeFileSync(join(subDir, 'file2.txt'), 'Sub file content') + + const builder = new Builder() + + // Add directory to archive + builder.appendDirAll('my-dir', testDir) + + // Finish and verify + const archiveData = builder.finish() + t.not(archiveData, null, 'Should return archive data') + + const archive = new Archive(Buffer.from(archiveData!)) + const filePaths: string[] = [] + + for (const entry of archive.entries()) { + const path = entry.path() + if (path) { + filePaths.push(path) + } + } + + // Should contain the directory and its files + t.true(filePaths.some(p => p.includes('my-dir')), 'Should contain my-dir entries') + t.true(filePaths.some(p => p.includes('file1.txt')), 'Should contain file1.txt') + t.true(filePaths.some(p => p.includes('file2.txt')), 'Should contain file2.txt') + + // Clean up test directory + rmSync(testDir, { recursive: true, force: true }) +}) + +test('Builder - should handle mixed content types', (t) => { + if (process.env.NAPI_RS_FORCE_WASI) { + t.pass('Skipping mixed content types test on WASI') + return + } + // Create a temp file for testing + const tempFile = join(__dirname, 'mixed-test.txt') + writeFileSync(tempFile, 'File from disk') + + // Create a temp directory + const tempDir = join(__dirname, 'mixed-dir') + mkdirSync(tempDir, { recursive: true }) + writeFileSync(join(tempDir, 'dir-file.txt'), 'File in directory') + + const builder = new Builder() + + // Mix different append methods + builder.appendData('data-file.txt', Buffer.from('Data from memory')) + builder.appendFile('disk-file.txt', tempFile) + builder.appendDirAll('my-directory', tempDir) + + // Finish and verify + const archiveData = builder.finish() + t.not(archiveData, null, 'Should return archive data') + + const archive = new Archive(Buffer.from(archiveData!)) + const fileContents = new Map() + + for (const entry of archive.entries()) { + const path = entry.path() + if (path && !path.endsWith('/')) { // Skip directories + const content = entry.asBytes().toString('utf-8') + fileContents.set(path, content) + } + } + + t.is(fileContents.get('data-file.txt'), 'Data from memory', 'Should have data file content') + t.is(fileContents.get('disk-file.txt'), 'File from disk', 'Should have disk file content') + t.true(Array.from(fileContents.keys()).some(k => k.includes('dir-file.txt')), 'Should have directory file') + + // Clean up + unlinkSync(tempFile) + rmSync(tempDir, { recursive: true, force: true }) +}) + +test('Builder - should handle empty archive', (t) => { + const builder = new Builder() + + // Create empty archive + const archiveData = builder.finish() + t.not(archiveData, null, 'Should return archive data even when empty') + t.true(archiveData instanceof Array, 'Should return an array') + t.true(archiveData!.length > 0, 'Empty archive should still have tar headers') + + // For empty archives, we can verify that it's at least a valid tar structure + // by checking that it contains the expected tar end blocks (512 zero bytes x 2) + const buffer = Buffer.from(archiveData!) + t.true(buffer.length >= 1024, 'Empty tar should be at least 1024 bytes (2 zero blocks)') +}) + +test('Builder - should create archive compatible with existing Archive reader', (t) => { + const builder = new Builder() + + // Create archive with known content + const testData = { + 'readme.txt': 'This is a readme file', + 'config.json': JSON.stringify({ version: '1.0.0', name: 'test' }), + 'data.bin': Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04]) + } + + for (const [filename, content] of Object.entries(testData)) { + const data = content instanceof Buffer ? content : Buffer.from(content) + builder.appendData(filename, data) + } + + const archiveData = builder.finish() + t.not(archiveData, null, 'Should create archive data') + + // Test that Archive can read the created archive + const archive = new Archive(Buffer.from(archiveData!)) + const extractedFiles = new Map() + + for (const entry of archive.entries()) { + const path = entry.path() + if (path) { + extractedFiles.set(path, entry.asBytes()) + } + } + + // Verify all files were extracted correctly + t.is(extractedFiles.size, 3, 'Should extract all 3 files') + t.is(extractedFiles.get('readme.txt')?.toString('utf-8'), testData['readme.txt'], 'Readme content should match') + t.is(extractedFiles.get('config.json')?.toString('utf-8'), testData['config.json'], 'Config content should match') + t.deepEqual(Array.from(extractedFiles.get('data.bin')!), Array.from(testData['data.bin']), 'Binary data should match') +}) + +test('Builder - should handle large data', (t) => { + const builder = new Builder() + + // Create a large file (1MB) + const largeData = Buffer.alloc(1024 * 1024, 'A') + builder.appendData('large-file.txt', largeData) + + const archiveData = builder.finish() + t.not(archiveData, null, 'Should handle large files') + + const archive = new Archive(Buffer.from(archiveData!)) + let foundFile = false + + for (const entry of archive.entries()) { + if (entry.path() === 'large-file.txt') { + const extractedData = entry.asBytes() + t.is(extractedData.length, largeData.length, 'Extracted file should have same size') + t.is(extractedData[0], 65, 'First byte should be "A"') // ASCII 'A' = 65 + t.is(extractedData[extractedData.length - 1], 65, 'Last byte should be "A"') + foundFile = true + break + } + } + + t.true(foundFile, 'Should find the large file in archive') +}) + +test('Builder - should handle unicode filenames', (t) => { + const builder = new Builder() + + // Test various unicode filenames + const unicodeFiles = { + 'файл.txt': 'Russian filename', + '文件.txt': 'Chinese filename', + '🚀rocket.txt': 'Emoji filename', + 'café.txt': 'Accented filename', + } + + for (const [filename, content] of Object.entries(unicodeFiles)) { + builder.appendData(filename, Buffer.from(content)) + } + + const archiveData = builder.finish() + const archive = new Archive(Buffer.from(archiveData!)) + const extractedFiles = new Map() + + for (const entry of archive.entries()) { + const path = entry.path() + if (path) { + extractedFiles.set(path, entry.asBytes().toString('utf-8')) + } + } + + // Verify all unicode filenames are preserved + for (const [filename, expectedContent] of Object.entries(unicodeFiles)) { + t.is(extractedFiles.get(filename), expectedContent, `Should preserve unicode filename: ${filename}`) + } +}) + +test('Builder - should handle paths with subdirectories', (t) => { + const builder = new Builder() + + // Add files with directory structure + builder.appendData('root.txt', Buffer.from('Root file')) + builder.appendData('dir1/file1.txt', Buffer.from('File in dir1')) + builder.appendData('dir1/subdir/file2.txt', Buffer.from('File in subdir')) + builder.appendData('dir2/file3.txt', Buffer.from('File in dir2')) + + const archiveData = builder.finish() + const archive = new Archive(Buffer.from(archiveData!)) + const filePaths: string[] = [] + + for (const entry of archive.entries()) { + const path = entry.path() + if (path) { + filePaths.push(path) + } + } + + t.true(filePaths.includes('root.txt'), 'Should have root file') + t.true(filePaths.includes('dir1/file1.txt'), 'Should have file in dir1') + t.true(filePaths.includes('dir1/subdir/file2.txt'), 'Should have file in subdir') + t.true(filePaths.includes('dir2/file3.txt'), 'Should have file in dir2') +}) + +test('Builder - should work with file output and then read back', (t) => { + const outputPath = join(__dirname, 'builder-test-output.tar') + + // Create archive with file output + const builder = new Builder(outputPath) + builder.appendData('test-file.txt', Buffer.from('Test content for file output')) + const result = builder.finish() + + t.is(result, null, 'File output should return null') + + // Read back the created file + const archive = new Archive(outputPath) + let foundContent = '' + + for (const entry of archive.entries()) { + if (entry.path() === 'test-file.txt') { + foundContent = entry.asBytes().toString('utf-8') + break + } + } + + t.is(foundContent, 'Test content for file output', 'Should be able to read back file output') + + // Clean up + unlinkSync(outputPath) +}) diff --git a/index.d.ts b/index.d.ts index ed7f6c0..ffd24db 100644 --- a/index.d.ts +++ b/index.d.ts @@ -73,6 +73,46 @@ export declare class Archive { setIgnoreZeros(ignoreZeros: boolean): void } +export declare class Builder { + /** + * Create a new builder which will write to the specified output. + * The output can be a file path (string) or will create a buffer internally. + */ + constructor(output?: string | undefined | null) + /** + * Append a file from disk to this archive. + * + * This function will open the file specified by `src` and add it to the + * archive as `name`. The `name` specified is the name that will be used + * inside the archive. + */ + appendFile(name: string, src: string): void + /** + * Append a directory and all of its contents to this archive. + * + * This function will recursively add all files and directories in the + * specified `src` directory to the archive, preserving their relative + * paths under `name`. + */ + appendDirAll(name: string, src: string): void + /** + * Append raw data to this archive with the specified name. + * + * This function allows you to add arbitrary data to the archive with a + * specified filename. + */ + appendData(name: string, data: Uint8Array): void + /** + * Finalize the archive and return the resulting data. + * + * This function must be called to properly finish the archive. + * If a file path was provided during construction, this will flush + * and close the file. If no path was provided, this returns the + * archive data as a Buffer. + */ + finish(): Array | null +} + /** * This type extends JavaScript's `Iterator`, and so has the iterator helper * methods. It may extend the upcoming TypeScript `Iterator` class in the future. diff --git a/index.js b/index.js index 3f0b0fa..0d8bdcf 100644 --- a/index.js +++ b/index.js @@ -80,8 +80,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-android-arm64') const bindingPackageVersion = require('@napi-rs/tar-android-arm64/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -96,8 +96,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-android-arm-eabi') const bindingPackageVersion = require('@napi-rs/tar-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -116,8 +116,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-win32-x64-msvc') const bindingPackageVersion = require('@napi-rs/tar-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -132,8 +132,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-win32-ia32-msvc') const bindingPackageVersion = require('@napi-rs/tar-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -148,8 +148,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-win32-arm64-msvc') const bindingPackageVersion = require('@napi-rs/tar-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -167,8 +167,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-darwin-universal') const bindingPackageVersion = require('@napi-rs/tar-darwin-universal/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -183,8 +183,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-darwin-x64') const bindingPackageVersion = require('@napi-rs/tar-darwin-x64/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -199,8 +199,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-darwin-arm64') const bindingPackageVersion = require('@napi-rs/tar-darwin-arm64/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -219,8 +219,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-freebsd-x64') const bindingPackageVersion = require('@napi-rs/tar-freebsd-x64/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -235,8 +235,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-freebsd-arm64') const bindingPackageVersion = require('@napi-rs/tar-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -256,8 +256,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-linux-x64-musl') const bindingPackageVersion = require('@napi-rs/tar-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -272,8 +272,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-linux-x64-gnu') const bindingPackageVersion = require('@napi-rs/tar-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -290,8 +290,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-linux-arm64-musl') const bindingPackageVersion = require('@napi-rs/tar-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -306,8 +306,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-linux-arm64-gnu') const bindingPackageVersion = require('@napi-rs/tar-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -324,8 +324,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-linux-arm-musleabihf') const bindingPackageVersion = require('@napi-rs/tar-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -340,8 +340,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-linux-arm-gnueabihf') const bindingPackageVersion = require('@napi-rs/tar-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -358,8 +358,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-linux-riscv64-musl') const bindingPackageVersion = require('@napi-rs/tar-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -374,8 +374,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-linux-riscv64-gnu') const bindingPackageVersion = require('@napi-rs/tar-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -391,8 +391,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-linux-ppc64-gnu') const bindingPackageVersion = require('@napi-rs/tar-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -407,8 +407,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-linux-s390x-gnu') const bindingPackageVersion = require('@napi-rs/tar-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -427,8 +427,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-openharmony-arm64') const bindingPackageVersion = require('@napi-rs/tar-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -443,8 +443,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-openharmony-x64') const bindingPackageVersion = require('@napi-rs/tar-openharmony-x64/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -459,8 +459,8 @@ function requireNative() { try { const binding = require('@napi-rs/tar-openharmony-arm') const bindingPackageVersion = require('@napi-rs/tar-openharmony-arm/package.json').version - if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '1.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -509,6 +509,7 @@ if (!nativeBinding) { module.exports = nativeBinding module.exports.Archive = nativeBinding.Archive +module.exports.Builder = nativeBinding.Builder module.exports.Entries = nativeBinding.Entries module.exports.Entry = nativeBinding.Entry module.exports.Header = nativeBinding.Header diff --git a/src/lib.rs b/src/lib.rs index eafe0dd..717c3c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ use std::{ fs::File, - io::{BufReader, Cursor, Read}, + io::{BufReader, Cursor, Read, Write}, }; use napi::bindgen_prelude::{Either, Either4, Env, Reference}; @@ -227,3 +227,108 @@ impl Archive { self.inner.set_ignore_zeros(ignore_zeros); } } + +pub enum BuilderOutput { + File(File), + Buffer(Cursor>), +} + +impl Write for BuilderOutput { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + match self { + Self::File(file) => file.write(buf), + Self::Buffer(buffer) => buffer.write(buf), + } + } + + fn flush(&mut self) -> std::io::Result<()> { + match self { + Self::File(file) => file.flush(), + Self::Buffer(buffer) => buffer.flush(), + } + } +} + +#[napi] +pub struct Builder { + inner: tar::Builder, +} + +#[napi] +impl Builder { + #[napi(constructor)] + /// Create a new builder which will write to the specified output. + /// The output can be a file path (string) or will create a buffer internally. + pub fn new(output: Option) -> napi::Result { + let builder_output = match output { + Some(path) => BuilderOutput::File(File::create(path)?), + None => BuilderOutput::Buffer(Cursor::new(Vec::new())), + }; + + Ok(Self { + inner: tar::Builder::new(builder_output), + }) + } + + #[napi] + /// Append a file from disk to this archive. + /// + /// This function will open the file specified by `src` and add it to the + /// archive as `name`. The `name` specified is the name that will be used + /// inside the archive. + pub fn append_file(&mut self, name: String, src: String) -> napi::Result<()> { + let mut file = File::open(src)?; + self.inner.append_file(name, &mut file)?; + Ok(()) + } + + #[napi] + /// Append a directory and all of its contents to this archive. + /// + /// This function will recursively add all files and directories in the + /// specified `src` directory to the archive, preserving their relative + /// paths under `name`. + pub fn append_dir_all(&mut self, name: String, src: String) -> napi::Result<()> { + self.inner.append_dir_all(name, src)?; + Ok(()) + } + + #[napi] + /// Append raw data to this archive with the specified name. + /// + /// This function allows you to add arbitrary data to the archive with a + /// specified filename. + pub fn append_data(&mut self, name: String, data: &[u8]) -> napi::Result<()> { + let mut header = tar::Header::new_gnu(); + header.set_size(data.len() as u64); + header.set_path(&name)?; + header.set_cksum(); + self.inner.append_data(&mut header, name, data)?; + Ok(()) + } + + #[napi] + /// Finalize the archive and return the resulting data. + /// + /// This function must be called to properly finish the archive. + /// If a file path was provided during construction, this will flush + /// and close the file. If no path was provided, this returns the + /// archive data as a Buffer. + pub fn finish(&mut self) -> napi::Result>> { + // We need to replace the inner builder to be able to consume it + let dummy_output = BuilderOutput::Buffer(Cursor::new(Vec::new())); + let builder = std::mem::replace(&mut self.inner, tar::Builder::new(dummy_output)); + + let inner = builder.into_inner()?; + match inner { + BuilderOutput::File(_) => { + // File-based output, nothing to return + Ok(None) + } + BuilderOutput::Buffer(cursor) => { + // Buffer-based output, return the data + Ok(Some(cursor.into_inner())) + } + } + } +} diff --git a/tar.wasi-browser.js b/tar.wasi-browser.js index f847808..12d6ad6 100644 --- a/tar.wasi-browser.js +++ b/tar.wasi-browser.js @@ -57,6 +57,7 @@ const { }) export default __napiModule.exports export const Archive = __napiModule.exports.Archive +export const Builder = __napiModule.exports.Builder export const Entries = __napiModule.exports.Entries export const Entry = __napiModule.exports.Entry export const Header = __napiModule.exports.Header diff --git a/tar.wasi.cjs b/tar.wasi.cjs index 9461a21..df564d1 100644 --- a/tar.wasi.cjs +++ b/tar.wasi.cjs @@ -109,6 +109,7 @@ const { instance: __napiInstance, module: __wasiModule, napiModule: __napiModule }) module.exports = __napiModule.exports module.exports.Archive = __napiModule.exports.Archive +module.exports.Builder = __napiModule.exports.Builder module.exports.Entries = __napiModule.exports.Entries module.exports.Entry = __napiModule.exports.Entry module.exports.Header = __napiModule.exports.Header