diff --git a/pro/src/sync.ts b/pro/src/sync.ts index c2e4f4e..4599fc3 100644 --- a/pro/src/sync.ts +++ b/pro/src/sync.ts @@ -28,6 +28,7 @@ import { } from "../../src/metadataOnRemote"; import { atWhichLevel, + checkValidName, getParentFolder, isHiddenPath, isSpecialFolderNameToSkip, @@ -211,6 +212,13 @@ const ensembleMixedEnties = async ( continue; } + const checkValidNameResult = checkValidName(key); + if (!checkValidNameResult.result) { + throw Error( + `your remote folder/file name is invalid: ${checkValidNameResult.reason}` + ); + } + finalMappings[key] = { key: key, remote: remoteCopied, @@ -280,6 +288,13 @@ const ensembleMixedEnties = async ( continue; } + const checkValidNameResult = checkValidName(key); + if (!checkValidNameResult.result) { + throw Error( + `your local folder/file name is invalid: ${checkValidNameResult.reason}` + ); + } + // TODO: abstraction leaking? const localCopied = await fsEncrypt.encryptEntity( copyEntityAndFixTimeFormat(local, serviceType) @@ -920,6 +935,8 @@ const getSyncPlanInplace = async ( keptFolder.delete("/"); keptFolder.delete(""); if (keptFolder.size > 0) { + console.error(sortedKeys); + console.error(mixedEntityMappings); throw Error(`unexpectedly keptFolder no decisions: ${[...keptFolder]}`); } diff --git a/src/misc.ts b/src/misc.ts index a0faba2..c1b61c4 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -742,3 +742,110 @@ export const getSha1 = async (x: ArrayBuffer, stringify: "base64" | "hex") => { } throw Error(`not supported stringify option = ${stringify}`); }; + +/** + * https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions + * https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa#invalidcharacters + */ +export const checkValidName = (x: string) => { + if (x === undefined || x === "") { + // what?? + return { + reason: "empty", + result: false, + }; + } + + // The following reserved characters: + const invalidChars = '*"<>:|?'.split(""); + for (const c of invalidChars) { + if (x.includes(c)) { + return { + reason: `reserved character: ${c}`, + result: false, + }; + } + } + + // directory component + for (const c of [".", ".."]) { + if ( + x === c || + x.endsWith(`/${c}`) || + x.startsWith(`${c}/`) || + x.includes(`/${c}/`) + ) { + return { + reason: `directory being ${c}`, + result: false, + }; + } + } + + // reserved file names + const reservedNames = [ + "CON", + "PRN", + "AUX", + "NUL", + "COM0", + "COM1", + "COM2", + "COM3", + "COM4", + "COM5", + "COM6", + "COM7", + "COM8", + "COM9", + "COM¹", + "COM²", + "COM³", + "LPT0", + "LPT1", + "LPT2", + "LPT3", + "LPT4", + "LPT5", + "LPT6", + "LPT7", + "LPT8", + "LPT9", + "LPT¹", + "LPT²", + "LPT³", + ]; + for (const f of reservedNames) { + if ( + x === f || + x.startsWith(`${f}.`) || + x.startsWith(`${f}/`) || + x.includes(`/${f}/`) || + x.endsWith(`/${f}`) || + x.includes(`/${f}.`) + ) { + return { + reason: `reserved folder/file name: ${f}`, + result: false, + }; + } + } + + // Do not end a file or directory name with a space or a period. + if ( + x.endsWith(" ") || + x.endsWith(".") || + x.includes(" /") || + x.includes("./") + ) { + return { + reason: `folder/file name ending with a space or a period`, + result: false, + }; + } + + return { + reason: "ok", + result: true, + }; +}; diff --git a/tests/misc.test.ts b/tests/misc.test.ts index 1379e0c..9e2a564 100644 --- a/tests/misc.test.ts +++ b/tests/misc.test.ts @@ -363,6 +363,82 @@ describe("Misc: split chunk ranges", () => { }); }); +describe("Misc: check valid file names", () => { + it("should be ok for normal file nmes", () => { + let item = "what/.hidden/what/what/what"; + assert.ok(misc.checkValidName(item).result); + + item = "ssss/%%%^^^$xxxx.md"; + assert.ok(misc.checkValidName(item).result); + }); + + it("should be not ok for reserved characters", () => { + let item = "a**"; + assert.ok(!misc.checkValidName(item).result); + + item = "a?*"; + assert.ok(!misc.checkValidName(item).result); + + item = "<>:"; + assert.ok(!misc.checkValidName(item).result); + + item = "<>:/ssss"; + assert.ok(!misc.checkValidName(item).result); + }); + + it("should be not ok for reserved names", () => { + let item = "CON"; + assert.ok(!misc.checkValidName(item).result); + + item = "CON.txt"; + assert.ok(!misc.checkValidName(item).result); + + item = "CON.md"; + assert.ok(!misc.checkValidName(item).result); + + item = "con"; // lower case is ok + assert.ok(misc.checkValidName(item).result); + + item = "CON/"; + assert.ok(!misc.checkValidName(item).result); + + item = "CON.dir/"; + assert.ok(!misc.checkValidName(item).result); + + item = "CON.dir.folder/"; + assert.ok(!misc.checkValidName(item).result); + + item = "xxx/CON"; + assert.ok(!misc.checkValidName(item).result); + + item = "xxx/CON.txt"; + assert.ok(!misc.checkValidName(item).result); + + item = "xxx/CON.txt.md"; + assert.ok(!misc.checkValidName(item).result); + }); + + it("should be not ok for invalid endings", () => { + let item = "xxx "; + assert.ok(!misc.checkValidName(item).result); + + item = "/xxx "; + assert.ok(!misc.checkValidName(item).result); + + item = "xxx.yyy."; + assert.ok(!misc.checkValidName(item).result); + + item = "xxx.yyy."; + assert.ok(!misc.checkValidName(item).result); + + item = "xxx /"; + assert.ok(!misc.checkValidName(item).result); + + item = "xxx.yyy./"; + assert.ok(!misc.checkValidName(item).result); + }); +}); + describe("Misc: Dropbox: should fix the folder name cases", () => { it("should do nothing on empty folders", () => { const input: any[] = [];