From 638a305f2ae8e040f756f477ac5d71bee5197192 Mon Sep 17 00:00:00 2001 From: hikagami0210 Date: Sun, 13 Jul 2025 18:10:51 +0900 Subject: [PATCH 1/2] eat: add filename validation to newArticles command --- src/commands/newArticles.ts | 7 +++ src/lib/filename-validator.test.ts | 73 ++++++++++++++++++++++++++++++ src/lib/filename-validator.ts | 40 ++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 src/lib/filename-validator.test.ts create mode 100644 src/lib/filename-validator.ts diff --git a/src/commands/newArticles.ts b/src/commands/newArticles.ts index fa6b0eb..e8f0208 100644 --- a/src/commands/newArticles.ts +++ b/src/commands/newArticles.ts @@ -1,5 +1,6 @@ import arg from "arg"; import { getFileSystemRepo } from "../lib/get-file-system-repo"; +import { validateFilename } from "../lib/filename-validator"; export const newArticles = async (argv: string[]) => { const args = arg({}, { argv }); @@ -8,6 +9,12 @@ export const newArticles = async (argv: string[]) => { if (args._.length > 0) { for (const basename of args._) { + const validation = validateFilename(basename); + if (!validation.isValid) { + console.error(`Error: ${validation.error}`); + continue; + } + const createdFileName = await fileSystemRepo.createItem(basename); if (createdFileName) { console.log(`created: ${createdFileName}.md`); diff --git a/src/lib/filename-validator.test.ts b/src/lib/filename-validator.test.ts new file mode 100644 index 0000000..2f28eb8 --- /dev/null +++ b/src/lib/filename-validator.test.ts @@ -0,0 +1,73 @@ +import { validateFilename } from "./filename-validator"; + +describe("validateFilename", () => { + it("should accept valid filenames", () => { + const validNames = [ + "test", + "test-article", + "test_article", + "test123", + "記事テスト", + "article.backup", + "my-awesome-article", + ]; + + validNames.forEach((name) => { + const result = validateFilename(name); + expect(result.isValid).toBe(true); + expect(result.error).toBeUndefined(); + }); + }); + + it("should reject empty or whitespace-only filenames", () => { + const invalidNames = ["", " ", " ", "\t", "\n"]; + + invalidNames.forEach((name) => { + const result = validateFilename(name); + expect(result.isValid).toBe(false); + expect(result.error).toBe("Filename is empty"); + }); + }); + + it("should reject filenames with invalid characters", () => { + const invalidNames = [ + "testfile", + "test:file", + 'test"file', + "test/file", + "test\\file", + "test|file", + "test?file", + "test*file", + "test\x00file", + ]; + + invalidNames.forEach((name) => { + const result = validateFilename(name); + expect(result.isValid).toBe(false); + expect(result.error).toBe( + 'Filename contains invalid characters: < > : " / \\ | ? * and control characters', + ); + }); + }); + + it("should reject filenames starting or ending with dots or spaces", () => { + const invalidNames = [ + ".test", + "test.", + " test", + "test ", + "..test", + "test..", + ]; + + invalidNames.forEach((name) => { + const result = validateFilename(name); + expect(result.isValid).toBe(false); + expect(result.error).toBe( + "Filename cannot start or end with a dot or space", + ); + }); + }); +}); diff --git a/src/lib/filename-validator.ts b/src/lib/filename-validator.ts new file mode 100644 index 0000000..09fe22f --- /dev/null +++ b/src/lib/filename-validator.ts @@ -0,0 +1,40 @@ +// eslint-disable-next-line no-control-regex -- include control characters +const INVALID_FILENAME_CHARS = /[<>:"/\\|?*\x00-\x1f]/; + +export interface FilenameValidationResult { + isValid: boolean; + error?: string; +} + +export function validateFilename(filename: string): FilenameValidationResult { + if (!filename || filename.trim().length === 0) { + return { + isValid: false, + error: "Filename is empty", + }; + } + + if (INVALID_FILENAME_CHARS.test(filename)) { + return { + isValid: false, + error: + 'Filename contains invalid characters: < > : " / \\ | ? * and control characters', + }; + } + + if ( + filename.startsWith(".") || + filename.endsWith(".") || + filename.startsWith(" ") || + filename.endsWith(" ") + ) { + return { + isValid: false, + error: "Filename cannot start or end with a dot or space", + }; + } + + return { + isValid: true, + }; +} From b5e4926bb60f5ca449313b9071182b74dfae8389 Mon Sep 17 00:00:00 2001 From: hikagami0210 Date: Fri, 18 Jul 2025 16:16:40 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E6=8C=87=E6=91=98=E4=BA=8B=E9=A0=85=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/newArticles.ts | 8 +++ src/lib/filename-validator.test.ts | 109 +++++++++++++---------------- src/lib/filename-validator.ts | 20 +++--- 3 files changed, 65 insertions(+), 72 deletions(-) diff --git a/src/commands/newArticles.ts b/src/commands/newArticles.ts index e8f0208..c9a5f66 100644 --- a/src/commands/newArticles.ts +++ b/src/commands/newArticles.ts @@ -6,12 +6,14 @@ export const newArticles = async (argv: string[]) => { const args = arg({}, { argv }); const fileSystemRepo = await getFileSystemRepo(); + let hasErrors = false; if (args._.length > 0) { for (const basename of args._) { const validation = validateFilename(basename); if (!validation.isValid) { console.error(`Error: ${validation.error}`); + hasErrors = true; continue; } @@ -20,6 +22,7 @@ export const newArticles = async (argv: string[]) => { console.log(`created: ${createdFileName}.md`); } else { console.error(`Error: '${basename}.md' is already exist`); + hasErrors = true; } } } else { @@ -28,6 +31,11 @@ export const newArticles = async (argv: string[]) => { console.log(`created: ${createdFileName}.md`); } else { console.error("Error: failed to create"); + hasErrors = true; } } + + if (hasErrors) { + process.exitCode = 1; + } }; diff --git a/src/lib/filename-validator.test.ts b/src/lib/filename-validator.test.ts index 2f28eb8..e72e2e9 100644 --- a/src/lib/filename-validator.test.ts +++ b/src/lib/filename-validator.test.ts @@ -1,73 +1,58 @@ -import { validateFilename } from "./filename-validator"; +import { + validateFilename, + ERROR_FILENAME_EMPTY, + ERROR_INVALID_CHARACTERS, + ERROR_INVALID_START_END, +} from "./filename-validator"; describe("validateFilename", () => { - it("should accept valid filenames", () => { - const validNames = [ - "test", - "test-article", - "test_article", - "test123", - "記事テスト", - "article.backup", - "my-awesome-article", - ]; - - validNames.forEach((name) => { - const result = validateFilename(name); - expect(result.isValid).toBe(true); - expect(result.error).toBeUndefined(); - }); - }); - - it("should reject empty or whitespace-only filenames", () => { - const invalidNames = ["", " ", " ", "\t", "\n"]; - - invalidNames.forEach((name) => { - const result = validateFilename(name); - expect(result.isValid).toBe(false); - expect(result.error).toBe("Filename is empty"); - }); + it.each([ + "test", + "test-article", + "test_article", + "test123", + "記事テスト", + "article.backup", + "my-awesome-article", + "test file", + ])("should accept valid filename '%s'", (name) => { + const result = validateFilename(name); + expect(result.isValid).toBe(true); + expect(result.error).toBeUndefined(); }); - it("should reject filenames with invalid characters", () => { - const invalidNames = [ - "testfile", - "test:file", - 'test"file', - "test/file", - "test\\file", - "test|file", - "test?file", - "test*file", - "test\x00file", - ]; - - invalidNames.forEach((name) => { + it.each(["", " ", " ", "\t", "\n"])( + "should reject empty or whitespace-only filename '%s'", + (name) => { const result = validateFilename(name); expect(result.isValid).toBe(false); - expect(result.error).toBe( - 'Filename contains invalid characters: < > : " / \\ | ? * and control characters', - ); - }); + expect(result.error).toBe(ERROR_FILENAME_EMPTY); + }, + ); + + it.each([ + "testfile", + "test:file", + 'test"file', + "test/file", + "test\\file", + "test|file", + "test?file", + "test*file", + "test\x00file", + ])("should reject filename with invalid characters '%s'", (name) => { + const result = validateFilename(name); + expect(result.isValid).toBe(false); + expect(result.error).toBe(ERROR_INVALID_CHARACTERS); }); - it("should reject filenames starting or ending with dots or spaces", () => { - const invalidNames = [ - ".test", - "test.", - " test", - "test ", - "..test", - "test..", - ]; - - invalidNames.forEach((name) => { + it.each([".test", "test.", " test", "test ", "..test", "test.."])( + "should reject filename starting or ending with dots or spaces '%s'", + (name) => { const result = validateFilename(name); expect(result.isValid).toBe(false); - expect(result.error).toBe( - "Filename cannot start or end with a dot or space", - ); - }); - }); + expect(result.error).toBe(ERROR_INVALID_START_END); + }, + ); }); diff --git a/src/lib/filename-validator.ts b/src/lib/filename-validator.ts index 09fe22f..0336b9f 100644 --- a/src/lib/filename-validator.ts +++ b/src/lib/filename-validator.ts @@ -1,6 +1,12 @@ // eslint-disable-next-line no-control-regex -- include control characters const INVALID_FILENAME_CHARS = /[<>:"/\\|?*\x00-\x1f]/; +export const ERROR_FILENAME_EMPTY = "Filename is empty"; +export const ERROR_INVALID_CHARACTERS = + 'Filename contains invalid characters: < > : " / \\ | ? * and control characters'; +export const ERROR_INVALID_START_END = + "Filename cannot start or end with a dot or space"; + export interface FilenameValidationResult { isValid: boolean; error?: string; @@ -10,27 +16,21 @@ export function validateFilename(filename: string): FilenameValidationResult { if (!filename || filename.trim().length === 0) { return { isValid: false, - error: "Filename is empty", + error: ERROR_FILENAME_EMPTY, }; } if (INVALID_FILENAME_CHARS.test(filename)) { return { isValid: false, - error: - 'Filename contains invalid characters: < > : " / \\ | ? * and control characters', + error: ERROR_INVALID_CHARACTERS, }; } - if ( - filename.startsWith(".") || - filename.endsWith(".") || - filename.startsWith(" ") || - filename.endsWith(" ") - ) { + if (/^[. ]|[. ]$/.test(filename)) { return { isValid: false, - error: "Filename cannot start or end with a dot or space", + error: ERROR_INVALID_START_END, }; }