From 99b217e3dde26b2865f8e127050083a198dfcd35 Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Sat, 8 Nov 2025 00:10:21 +0900 Subject: [PATCH 1/4] refactor: rename restoreFileName to preserveName for clarity - Updated all instances of `restoreFileName` to `preserveName` across the codebase, including in the README and CLI documentation. - Adjusted related types and constants to reflect the new naming convention, enhancing consistency and clarity in the image processing functionality. --- README.md | 2 +- packages/cli/README.md | 10 +++++----- packages/cli/src/commands/shuffle.ts | 4 ++-- packages/cli/src/types.ts | 2 +- packages/core/src/constants.ts | 2 +- packages/core/src/types.ts | 4 ++-- packages/node/src/fragmenter.test.ts | 14 +++++++------- packages/node/src/fragmenter.ts | 9 ++++----- packages/node/src/index.test.ts | 12 ++++++------ packages/node/src/restorer.test.ts | 4 ++-- 10 files changed, 31 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 65565b7..1bb7028 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ manifest.json: "blockSize": 2, "prefix": "img", "seed": 72411, - "restoreFileName": false + "preserveName": false }, "images": [ { diff --git a/packages/cli/README.md b/packages/cli/README.md index b7485a2..289b6ea 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -49,7 +49,7 @@ image-shield shuffle -o [options] | `-b, --block-size ` | Pixel block size (positive integer) | ❌ | 10 | | `-p, --prefix ` | Prefix for fragment files | ❌ | "fragment" | | `-s, --seed ` | Random seed (integer) | ❌ | auto-generated | -| `--restore-filename` | Restore original file names when restoring | ❌ | false | +| `--preserve-name` | Preserve original file names | ❌ | false | #### Examples @@ -60,7 +60,7 @@ image-shield shuffle image1.jpg image2.png -o ./fragments **Custom configuration:** ```bash -image-shield shuffle *.jpg -o ./output -b 20 -p "my_fragment" --restore-filename +image-shield shuffle *.jpg -o ./output -b 20 -p "my_fragment" --preserve-name ``` **With seed for reproducible results:** @@ -127,7 +127,7 @@ The CLI provides clear error messages for common issues: 2. **Fragment images:** ```bash - image-shield shuffle images/photo1.jpg images/photo2.png -o ./backup --restore-filename + image-shield shuffle images/photo1.jpg images/photo2.png -o ./backup --preserve-name ``` ``` 🔀 Starting image fragmentation... @@ -166,7 +166,7 @@ image-shield shuffle sensitive/*.jpg \ -b 5 \ -p "secure_chunk" \ -s 42 \ - --restore-filename + --preserve-name # The output will use smaller blocks (5x5 pixels) and custom naming ``` @@ -186,7 +186,7 @@ BACKUP_DIR="./backup" echo "Creating backup..." image-shield shuffle "$IMAGES_DIR"/*.{jpg,png} \ -o "$BACKUP_DIR" \ - --restore-filename + --preserve-name if [ $? -eq 0 ]; then echo "✅ Backup completed successfully" diff --git a/packages/cli/src/commands/shuffle.ts b/packages/cli/src/commands/shuffle.ts index 05eff7c..1640fa6 100644 --- a/packages/cli/src/commands/shuffle.ts +++ b/packages/cli/src/commands/shuffle.ts @@ -29,7 +29,7 @@ export function registerShuffleCommand(program: Command): void { } return num; }) - .option("--restore-filename", "Restore original file names") + .option("--preserve-name", "Preserve original file names") .action(handleShuffleCommand); } @@ -52,7 +52,7 @@ async function handleShuffleCommand( if (options.blockSize !== undefined) config.blockSize = options.blockSize; if (options.prefix !== undefined) config.prefix = options.prefix; if (options.seed !== undefined) config.seed = options.seed; - if (options.restoreFilename) config.restoreFileName = true; + if (options.preserveName) config.preserveName = true; await ImageShield.shuffle({ imagePaths, diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 132fb1c..60d2c47 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -3,7 +3,7 @@ export interface ShuffleOptions { blockSize?: number; prefix?: string; seed?: number; - restoreFilename?: boolean; + preserveName?: boolean; } export interface RestoreOptions { diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 395089c..b1d717d 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -3,5 +3,5 @@ export const MANIFEST_FILE_NAME = "manifest.json"; export const DEFAULT_FRAGMENTATION_CONFIG = { BLOCK_SIZE: 2, PREFIX: "img", - RESTORE_FILE_NAME: false, + PRESERVE_NAME: false, }; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 27424df..5e30eb5 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -23,8 +23,8 @@ export interface FragmentationConfig { prefix?: string; /** Random seed (auto-generated if not specified) */ seed?: number | string; - /** Restore original file name (optional, default: false) */ - restoreFileName?: boolean; + /** Preserve original file name (optional, default: false) */ + preserveName?: boolean; } /** diff --git a/packages/node/src/fragmenter.test.ts b/packages/node/src/fragmenter.test.ts index cc78d7b..190cacc 100644 --- a/packages/node/src/fragmenter.test.ts +++ b/packages/node/src/fragmenter.test.ts @@ -115,7 +115,7 @@ describe("ImageFragmenter", () => { blockSize: 8, prefix: "test", seed: "test-seed", - restoreFileName: true, + preserveName: true, }; const fragmenter = new ImageFragmenter(config); expect(fragmenter).toBeInstanceOf(ImageFragmenter); @@ -159,7 +159,7 @@ describe("ImageFragmenter", () => { blockSize: 2, prefix: "testprefix", seed: "test-seed-123", - restoreFileName: true, + preserveName: true, }; const fragmenter = new ImageFragmenter(config); @@ -171,7 +171,7 @@ describe("ImageFragmenter", () => { expect(result.manifest.config.blockSize).toBe(2); expect(result.manifest.config.prefix).toBe("testprefix"); expect(result.manifest.config.seed).toBe("test-seed-123"); - expect(result.manifest.config.restoreFileName).toBe(true); + expect(result.manifest.config.preserveName).toBe(true); }); test("creates valid fragment images", async () => { @@ -209,7 +209,7 @@ describe("ImageFragmenter", () => { ); }); - test("throws error on duplicate file names when restoreFileName is enabled", async () => { + test("throws error on duplicate file names when preserveName is enabled", async () => { // Create two files with the same base name in different directories const tmpDir2 = path.join(tmpdir(), "fragmenter_test_tmp2"); if (!fs.existsSync(tmpDir2)) fs.mkdirSync(tmpDir2); @@ -218,7 +218,7 @@ describe("ImageFragmenter", () => { await fs.promises.copyFile(testImagePath, duplicatePath); const fragmenter = new ImageFragmenter({ - restoreFileName: true, + preserveName: true, }); try { @@ -243,8 +243,8 @@ describe("ImageFragmenter", () => { expect(result.manifest.config.prefix).toBe( DEFAULT_FRAGMENTATION_CONFIG.PREFIX, ); - expect(result.manifest.config.restoreFileName).toBe( - DEFAULT_FRAGMENTATION_CONFIG.RESTORE_FILE_NAME, + expect(result.manifest.config.preserveName).toBe( + DEFAULT_FRAGMENTATION_CONFIG.PRESERVE_NAME, ); expect(result.manifest.config.seed).toBeDefined(); // Should be auto-generated }); diff --git a/packages/node/src/fragmenter.ts b/packages/node/src/fragmenter.ts index f6f4701..3d3ee50 100644 --- a/packages/node/src/fragmenter.ts +++ b/packages/node/src/fragmenter.ts @@ -26,9 +26,8 @@ export class ImageFragmenter { blockSize: config.blockSize ?? DEFAULT_FRAGMENTATION_CONFIG.BLOCK_SIZE, prefix: config.prefix ?? DEFAULT_FRAGMENTATION_CONFIG.PREFIX, seed: config.seed || SeededRandom.generateSeed(), - restoreFileName: - config.restoreFileName ?? - DEFAULT_FRAGMENTATION_CONFIG.RESTORE_FILE_NAME, + preserveName: + config.preserveName ?? DEFAULT_FRAGMENTATION_CONFIG.PRESERVE_NAME, }; } @@ -98,7 +97,7 @@ export class ImageFragmenter { } private _validateFileNames(imageInfos: ImageInfo[]): void { - if (!this.config.restoreFileName || imageInfos.length <= 1) { + if (!this.config.preserveName || imageInfos.length <= 1) { return; } @@ -163,7 +162,7 @@ export class ImageFragmenter { c: 4, // Always use 4 channels (RGBA) for generated PNG x: blockCountX, y: blockCountY, - name: this.config.restoreFileName + name: this.config.preserveName ? fileNameWithoutExtension(imagePath) : undefined, }; diff --git a/packages/node/src/index.test.ts b/packages/node/src/index.test.ts index febe4bc..20040b1 100644 --- a/packages/node/src/index.test.ts +++ b/packages/node/src/index.test.ts @@ -132,8 +132,8 @@ describe("ImageShield (integration)", () => { }); }); -describe("ImageShield (restoreFileName integration)", () => { - const tmpDir = path.join(tmpdir(), "index_test_tmp_restoreFileName"); +describe("ImageShield (preserveName integration)", () => { + const tmpDir = path.join(tmpdir(), "index_test_tmp_preserveName"); const originalImages = [ Buffer.from([ // 2x2 RGBA image 1 @@ -168,10 +168,10 @@ describe("ImageShield (restoreFileName integration)", () => { await image.write(filePath, JimpMime.png); imagePaths.push(filePath); } - // Fragment images using ImageShield.shuffle (with restoreFileName) + // Fragment images using ImageShield.shuffle (with preserveName) await ImageShield.shuffle({ imagePaths, - config: { blockSize, prefix, restoreFileName: true }, + config: { blockSize, prefix, preserveName: true }, outputDir: tmpDir, }); // Find manifest and fragment files @@ -214,9 +214,9 @@ describe("ImageShield (restoreFileName integration)", () => { if (fs.existsSync(tmpDir)) fs.rmdirSync(tmpDir); }); - test("manifest images[].name contains original file name when restoreFileName=true", () => { + test("manifest images[].name contains original file name when preserveName=true", () => { expect(manifest).toBeDefined(); - expect(manifest?.config.restoreFileName).toBe(true); + expect(manifest?.config.preserveName).toBe(true); expect(Array.isArray(manifest?.images)).toBe(true); for (let i = 0; i < imagePaths.length; i++) { const expectedName = path.parse(imagePaths[i]).name; diff --git a/packages/node/src/restorer.test.ts b/packages/node/src/restorer.test.ts index 6165037..c418037 100644 --- a/packages/node/src/restorer.test.ts +++ b/packages/node/src/restorer.test.ts @@ -343,11 +343,11 @@ describe("ImageRestorer", () => { }); describe("integration with different configurations", () => { - test("works with restoreFileName enabled", async () => { + test("works with preserveName enabled", async () => { const fragmenter = new ImageFragmenter({ blockSize: 2, seed: "test-seed", - restoreFileName: true, + preserveName: true, }); const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ testImagePath, From de456d482324bd4a6265c1cc44df6de7461d9b84 Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Sat, 8 Nov 2025 00:12:36 +0900 Subject: [PATCH 2/4] refactor: remove rootDir from CLI tsconfig for improved configuration - Eliminated the rootDir property from the tsconfig.json file in the CLI package to simplify the TypeScript configuration and enhance compatibility with module resolution. --- packages/cli/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index e3f0156..a8ce713 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -4,7 +4,6 @@ "noEmit": true, "module": "ESNext", "moduleResolution": "bundler", - "rootDir": "./src", "baseUrl": ".", "paths": { "@image-shield/core": ["../core/src"], From 76cee5e3c84c33e7b42a3e78feae56d3858e44e0 Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Sat, 8 Nov 2025 01:18:40 +0900 Subject: [PATCH 3/4] feat: implement file name encoding and decoding for enhanced compatibility - Added `encodeFileName` and `decodeFileName` functions to handle base64 encoding and decoding of file names, ensuring safe storage across platforms. - Updated `generateRestoredOriginalFileName` to utilize the new encoding functions, improving backward compatibility. - Enhanced tests to verify encoding and decoding functionality, including handling of special characters. - Adjusted type definitions to reflect that the original file name may now be stored in base64 format. --- packages/core/src/helpers.test.ts | 40 +++++++++++++++++++++++-- packages/core/src/helpers.ts | 47 +++++++++++++++++++++++++++++- packages/core/src/types.ts | 2 +- packages/node/src/fragmenter.ts | 18 +++++++++--- packages/node/src/index.test.ts | 9 +++++- packages/node/src/restorer.test.ts | 2 +- 6 files changed, 108 insertions(+), 10 deletions(-) diff --git a/packages/core/src/helpers.test.ts b/packages/core/src/helpers.test.ts index e2d08dc..25a7767 100644 --- a/packages/core/src/helpers.test.ts +++ b/packages/core/src/helpers.test.ts @@ -1,4 +1,6 @@ import { + decodeFileName, + encodeFileName, generateFileName, generateFragmentFileName, generateRestoredFileName, @@ -95,9 +97,43 @@ describe("generateRestoredFileName", () => { }); }); +describe("encodeFileName / decodeFileName", () => { + test("should encode and decode file names correctly", () => { + const originalName = "test-image.png"; + const encoded = encodeFileName(originalName); + expect(encoded).toBeTruthy(); + expect(encoded).not.toBe(originalName); + expect(decodeFileName(encoded)).toBe(originalName); + }); + + test("should handle special characters", () => { + const specialNames = [ + "画像_テスト.png", + "test-image-日本語.png", + "file with spaces.png", + "file@#$%^&.png", + ]; + for (const name of specialNames) { + const encoded = encodeFileName(name); + expect(decodeFileName(encoded)).toBe(name); + } + }); +}); + describe("generateRestoredOriginalFileName", () => { - test("should generate original file name when name exists", () => { - const imageInfo = { name: "original" } as ManifestData["images"][number]; + test("should generate original file name when name exists (base64 encoded)", () => { + const originalName = "original"; + const encodedName = encodeFileName(originalName); + const imageInfo = { + name: encodedName, + } as ManifestData["images"][number]; + expect(generateRestoredOriginalFileName(imageInfo)).toBe("original.png"); + }); + + test("should handle backward compatibility (non-encoded names)", () => { + const imageInfo = { + name: "b3JpZ2luYWw=", + } as ManifestData["images"][number]; expect(generateRestoredOriginalFileName(imageInfo)).toBe("original.png"); }); diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 15e4fec..bfd3c50 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -1,5 +1,41 @@ import type { ManifestData } from "./types"; +/** + * Encode file name to base64 for safe storage (cross-platform) + * @param name - Original file name + * @returns Base64 encoded file name + */ +export function encodeFileName(name: string): string { + // Use TextEncoder for UTF-8 encoding (available in both browser and Node.js) + const encoder = new TextEncoder(); + const bytes = encoder.encode(name); + // Convert bytes to binary string for btoa (cross-platform) + // Use loop to avoid stack overflow with large arrays + let binaryString = ""; + for (let i = 0; i < bytes.length; i++) { + const byte = bytes[i]; + if (byte !== undefined) { + binaryString += String.fromCharCode(byte); + } + } + return btoa(binaryString); +} + +/** + * Decode file name from base64 (cross-platform) + * @param encodedName - Base64 encoded file name + * @returns Decoded original file name + */ +export function decodeFileName(encodedName: string): string { + // Use atob for base64 decoding (cross-platform) + const binaryString = atob(encodedName); + // Convert binary string to bytes + const bytes = Uint8Array.from(binaryString, (c) => c.charCodeAt(0)); + // Use TextDecoder for UTF-8 decoding (available in both browser and Node.js) + const decoder = new TextDecoder(); + return decoder.decode(bytes); +} + /** * Generate a file name with prefix, 1-based zero-padded index, and extension * @param manifest - Manifest data @@ -65,5 +101,14 @@ export function generateRestoredFileName( export function generateRestoredOriginalFileName( imageInfo: ManifestData["images"][number], ): string | undefined { - return imageInfo.name ? `${imageInfo.name}.png` : undefined; + if (!imageInfo.name) { + return undefined; + } + try { + const decodedName = decodeFileName(imageInfo.name); + return decodedName ? `${decodedName}.png` : undefined; + } catch { + // Fallback: if decoding fails, treat as already decoded (backward compatibility) + return `${imageInfo.name}.png`; + } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 5e30eb5..de8de86 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -42,7 +42,7 @@ export interface ImageInfo { x: number; /** Number of blocks Y */ y: number; - /** Original file name (optional) */ + /** Original file name in base64 encoding (optional) */ name?: string; } diff --git a/packages/node/src/fragmenter.ts b/packages/node/src/fragmenter.ts index 3d3ee50..8e265a4 100644 --- a/packages/node/src/fragmenter.ts +++ b/packages/node/src/fragmenter.ts @@ -5,6 +5,8 @@ import { type ImageInfo, type ManifestData, calcBlocksPerFragment, + decodeFileName, + encodeFileName, } from "@image-shield/core"; import { SeededRandom, shuffle } from "@tuki0918/seeded-shuffle"; import { blocksToPngImage, imageFileToBlocks } from "./block"; @@ -105,10 +107,18 @@ export class ImageFragmenter { for (const info of imageInfos) { if (info.name !== undefined) { - if (nameSet.has(info.name)) { - throw new Error(`Duplicate file name detected: ${info.name}`); + // Decode base64 to get original name for comparison + let decodedName: string; + try { + decodedName = decodeFileName(info.name); + } catch { + // If decoding fails, treat as already decoded (backward compatibility) + decodedName = info.name; } - nameSet.add(info.name); + if (nameSet.has(decodedName)) { + throw new Error(`Duplicate file name detected: ${decodedName}`); + } + nameSet.add(decodedName); } } } @@ -163,7 +173,7 @@ export class ImageFragmenter { x: blockCountX, y: blockCountY, name: this.config.preserveName - ? fileNameWithoutExtension(imagePath) + ? encodeFileName(fileNameWithoutExtension(imagePath)) : undefined, }; diff --git a/packages/node/src/index.test.ts b/packages/node/src/index.test.ts index 20040b1..2e33172 100644 --- a/packages/node/src/index.test.ts +++ b/packages/node/src/index.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import type { ManifestData } from "@image-shield/core"; import { + decodeFileName, generateFragmentFileName, generateRestoredFileName, } from "@image-shield/core"; @@ -220,7 +221,13 @@ describe("ImageShield (preserveName integration)", () => { expect(Array.isArray(manifest?.images)).toBe(true); for (let i = 0; i < imagePaths.length; i++) { const expectedName = path.parse(imagePaths[i]).name; - expect(manifest?.images[i].name).toBe(expectedName); + const encodedName = manifest?.images[i]?.name; + expect(encodedName).toBeTruthy(); + // Decode base64 encoded name + if (encodedName) { + const decodedName = decodeFileName(encodedName); + expect(decodedName).toBe(expectedName); + } } }); diff --git a/packages/node/src/restorer.test.ts b/packages/node/src/restorer.test.ts index c418037..68c3576 100644 --- a/packages/node/src/restorer.test.ts +++ b/packages/node/src/restorer.test.ts @@ -360,7 +360,7 @@ describe("ImageRestorer", () => { ); expect(restoredImages).toHaveLength(1); - expect(manifest.images[0].name).toBe("test_image"); + expect(manifest.images[0].name).toBe("dGVzdF9pbWFnZQ=="); }); test("works with custom prefix", async () => { From 877c56e46f8a92433691e180e030fbd7eb9c7cfc Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Sat, 8 Nov 2025 01:46:18 +0900 Subject: [PATCH 4/4] refactor: simplify byte handling in encodeFileName function - Removed unnecessary undefined check in the encodeFileName function, streamlining the byte-to-string conversion process for improved performance and clarity. --- packages/core/src/helpers.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index bfd3c50..1389630 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -14,9 +14,7 @@ export function encodeFileName(name: string): string { let binaryString = ""; for (let i = 0; i < bytes.length; i++) { const byte = bytes[i]; - if (byte !== undefined) { - binaryString += String.fromCharCode(byte); - } + binaryString += String.fromCharCode(byte); } return btoa(binaryString); }