diff --git a/frontend/src/components/entry/entryForm/EntryFormSchema.test.ts b/frontend/src/components/entry/entryForm/EntryFormSchema.test.ts index ee6d2e3c8..4c20345f3 100644 --- a/frontend/src/components/entry/entryForm/EntryFormSchema.test.ts +++ b/frontend/src/components/entry/entryForm/EntryFormSchema.test.ts @@ -248,6 +248,47 @@ describe("schema", () => { expect(() => schema.parse(value)).toThrow(); }); + test("validation fails if name contains 4-byte characters", () => { + const value = { + ...baseValue, + name: "テă‚čト😊", // Contains emoji (4-byte character) + }; + + expect(() => schema.parse(value)).toThrow(); + }); + + test("validation fails if string attribute value contains 4-byte characters", () => { + const value = { + ...baseValue, + attrs: { + string: { + ...baseValue.attrs.string, + value: { + asString: "テă‚čト🚀", // Contains emoji (4-byte character) + }, + }, + }, + }; + + expect(() => schema.parse(value)).toThrow(); + }); + + test("validation fails if array string attribute value contains 4-byte characters", () => { + const value = { + ...baseValue, + attrs: { + arrayString: { + ...baseValue.attrs.arrayString, + value: { + asArrayString: [{ value: "テă‚čト🎼" }], // Contains emoji (4-byte character) + }, + }, + }, + }; + + expect(() => schema.parse(value)).toThrow(); + }); + test("validation fails if array-string attr value is mandatory and empty", () => { const value = { ...baseValue, diff --git a/frontend/src/components/entry/entryForm/EntryFormSchema.ts b/frontend/src/components/entry/entryForm/EntryFormSchema.ts index 6428a3a82..61508857c 100644 --- a/frontend/src/components/entry/entryForm/EntryFormSchema.ts +++ b/frontend/src/components/entry/entryForm/EntryFormSchema.ts @@ -6,6 +6,25 @@ import { EditableEntry } from "./EditableEntry"; import { AttributeTypes } from "services/Constants"; import { schemaForType } from "services/ZodSchemaUtil"; +// Function to detect 4-byte characters (characters outside the BMP - Basic Multilingual Plane) +const hasFourByteChars = (value: string): boolean => { + // Check for surrogate pairs (4-byte characters are represented as surrogate pairs in UTF-16) + for (let i = 0; i < value.length; i++) { + const code = value.charCodeAt(i); + if (code >= 0xd800 && code <= 0xdbff) { + // High surrogate + if (i + 1 < value.length) { + const nextCode = value.charCodeAt(i + 1); + if (nextCode >= 0xdc00 && nextCode <= 0xdfff) { + // Low surrogate - this is a 4-byte character (surrogate pair) + return true; + } + } + } + } + return false; +}; + // A schema that's compatible with existing types // TODO rethink it, e.g. consider to use union as a type of value export const schema = schemaForType()( @@ -15,6 +34,9 @@ export const schema = schemaForType()( .trim() .min(1, "ă‚ąă‚€ăƒ†ăƒ ćăŻćż…é ˆă§ă™") .max(200, "ă‚ąă‚€ăƒ†ăƒ ćăŒć€§ăă™ăŽăŸă™") + .refine((value) => !hasFourByteChars(value), { + message: "äœżç”šă§ăăȘă„æ–‡ć­—ăŒć«ăŸă‚ŒăŠă„ăŸă™", + }) .default(""), schema: z.object({ id: z.number(), @@ -38,19 +60,29 @@ export const schema = schemaForType()( asString: z .string() .max(1 << 16, "ć±žæ€§ăźć€€ăŒć€§ăă™ăŽăŸă™") + .refine((value) => !hasFourByteChars(value), { + message: "äœżç”šă§ăăȘă„æ–‡ć­—ăŒć«ăŸă‚ŒăŠă„ăŸă™", + }) .default("") .optional(), asArrayString: z .array( z.object({ - value: z.string().max(1 << 16, "ć±žæ€§ăźć€€ăŒć€§ăă™ăŽăŸă™"), + value: z + .string() + .max(1 << 16, "ć±žæ€§ăźć€€ăŒć€§ăă™ăŽăŸă™") + .refine((value) => !hasFourByteChars(value), { + message: "äœżç”šă§ăăȘă„æ–‡ć­—ăŒć«ăŸă‚ŒăŠă„ăŸă™", + }), }), ) .optional(), asObject: z .object({ id: z.number(), - name: z.string(), + name: z.string().refine((value) => !hasFourByteChars(value), { + message: "äœżç”šă§ăăȘă„æ–‡ć­—ăŒć«ăŸă‚ŒăŠă„ăŸă™", + }), }) .nullable() .optional(), @@ -58,17 +90,27 @@ export const schema = schemaForType()( .array( z.object({ id: z.number(), - name: z.string(), + name: z + .string() + .refine((value) => !hasFourByteChars(value), { + message: "äœżç”šă§ăăȘă„æ–‡ć­—ăŒć«ăŸă‚ŒăŠă„ăŸă™", + }), }), ) .optional(), asNamedObject: z .object({ - name: z.string(), + name: z.string().refine((value) => !hasFourByteChars(value), { + message: "äœżç”šă§ăăȘă„æ–‡ć­—ăŒć«ăŸă‚ŒăŠă„ăŸă™", + }), object: z .object({ id: z.number(), - name: z.string(), + name: z + .string() + .refine((value) => !hasFourByteChars(value), { + message: "äœżç”šă§ăăȘă„æ–‡ć­—ăŒć«ăŸă‚ŒăŠă„ăŸă™", + }), }) .nullable() .default(null), @@ -77,11 +119,19 @@ export const schema = schemaForType()( asArrayNamedObject: z .array( z.object({ - name: z.string(), + name: z + .string() + .refine((value) => !hasFourByteChars(value), { + message: "äœżç”šă§ăăȘă„æ–‡ć­—ăŒć«ăŸă‚ŒăŠă„ăŸă™", + }), object: z .object({ id: z.number(), - name: z.string(), + name: z + .string() + .refine((value) => !hasFourByteChars(value), { + message: "äœżç”šă§ăăȘă„æ–‡ć­—ăŒć«ăŸă‚ŒăŠă„ăŸă™", + }), }) .nullable() .default(null), @@ -92,7 +142,9 @@ export const schema = schemaForType()( asGroup: z .object({ id: z.number(), - name: z.string(), + name: z.string().refine((value) => !hasFourByteChars(value), { + message: "äœżç”šă§ăăȘă„æ–‡ć­—ăŒć«ăŸă‚ŒăŠă„ăŸă™", + }), }) .nullable() .optional(), @@ -100,14 +152,20 @@ export const schema = schemaForType()( .array( z.object({ id: z.number(), - name: z.string(), + name: z + .string() + .refine((value) => !hasFourByteChars(value), { + message: "äœżç”šă§ăăȘă„æ–‡ć­—ăŒć«ăŸă‚ŒăŠă„ăŸă™", + }), }), ) .optional(), asRole: z .object({ id: z.number(), - name: z.string(), + name: z.string().refine((value) => !hasFourByteChars(value), { + message: "äœżç”šă§ăăȘă„æ–‡ć­—ăŒć«ăŸă‚ŒăŠă„ăŸă™", + }), }) .nullable() .optional(), @@ -115,7 +173,11 @@ export const schema = schemaForType()( .array( z.object({ id: z.number(), - name: z.string(), + name: z + .string() + .refine((value) => !hasFourByteChars(value), { + message: "äœżç”šă§ăăȘă„æ–‡ć­—ăŒć«ăŸă‚ŒăŠă„ăŸă™", + }), }), ) .optional(),