From d04a51356dee5f8f24e1ba0dac33307e2d15c164 Mon Sep 17 00:00:00 2001 From: AboEl3iz Date: Fri, 4 Sep 2026 12:34:14 +0300 Subject: [PATCH] fix(contract-psl): surface invalid UTF-8 schema read failures and exit non-zero (#30198) Signed-off-by: AboEl3iz --- .../2-authoring/contract-psl/src/provider.ts | 5 ++- .../2-authoring/contract-psl/src/provider.ts | 5 ++- .../contract-psl/test/provider.test.ts | 33 ++++++++++++++++ .../invalid-utf8-schema.e2e.test.ts | 39 +++++++++++++++++++ 4 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 test/integration/test/cli-journeys/invalid-utf8-schema.e2e.test.ts diff --git a/packages/2-mongo-family/2-authoring/contract-psl/src/provider.ts b/packages/2-mongo-family/2-authoring/contract-psl/src/provider.ts index 99439ec7dfb3..abb6b100265d 100644 --- a/packages/2-mongo-family/2-authoring/contract-psl/src/provider.ts +++ b/packages/2-mongo-family/2-authoring/contract-psl/src/provider.ts @@ -63,9 +63,10 @@ export function mongoContract(schemaPath: string, options?: MongoContractOptions } let schema: string; try { - schema = await readFile(absoluteSchemaPath, 'utf-8'); + const rawBuffer = await readFile(absoluteSchemaPath); + schema = new TextDecoder('utf-8', { fatal: true }).decode(rawBuffer); } catch (error) { - const message = String(error); + const message = error instanceof Error ? error.message : String(error); return notOk({ summary: `Failed to read Prisma schema at "${schemaPath}"`, diagnostics: [ diff --git a/packages/2-sql/2-authoring/contract-psl/src/provider.ts b/packages/2-sql/2-authoring/contract-psl/src/provider.ts index 6ba23296fc18..0af7c60eaec1 100644 --- a/packages/2-sql/2-authoring/contract-psl/src/provider.ts +++ b/packages/2-sql/2-authoring/contract-psl/src/provider.ts @@ -102,9 +102,10 @@ export function prismaContract(schemaPath: string, options: PrismaContractOption } let schema: string; try { - schema = await readFile(absoluteSchemaPath, 'utf-8'); + const rawBuffer = await readFile(absoluteSchemaPath); + schema = new TextDecoder('utf-8', { fatal: true }).decode(rawBuffer); } catch (error) { - const message = String(error); + const message = error instanceof Error ? error.message : String(error); return notOk({ summary: `Failed to read Prisma schema at "${schemaPath}"`, diagnostics: [ diff --git a/packages/2-sql/2-authoring/contract-psl/test/provider.test.ts b/packages/2-sql/2-authoring/contract-psl/test/provider.test.ts index 33e874f628b2..6a613a43e57a 100644 --- a/packages/2-sql/2-authoring/contract-psl/test/provider.test.ts +++ b/packages/2-sql/2-authoring/contract-psl/test/provider.test.ts @@ -1,3 +1,4 @@ +import { Buffer } from 'node:buffer'; import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { applySpecifierDefaultControlPolicy } from '@internal/contract/apply-specifier-default-control-policy'; @@ -449,6 +450,38 @@ model Other { expect(codes).toContain('PSL_DUPLICATE_DECLARATION'); expect(codes).toContain('PSL_UNSUPPORTED_FIELD_TYPE'); }); + + it('returns PSL_SCHEMA_READ_FAILED diagnostic when schema file contains invalid UTF-8 bytes', async () => { + const tempDir = await mkdtemp(join(tmpdir(), 'psl-provider-utf8-')); + tempDirs.push(tempDir); + const schemaPath = join(tempDir, 'schema.prisma'); + await writeFile( + schemaPath, + Buffer.concat([ + Buffer.from('model User {\n id Int @id // comment with invalid byte: ', 'utf-8'), + Buffer.from([0x97]), + Buffer.from('\n}\n', 'utf-8'), + ]), + ); + + process.chdir(tempDir); + const contract = prismaContract('./schema.prisma', baseOptions); + const result = await contract.source.load( + createPostgresTestContext({ resolvedInputs: [schemaPath] }), + ); + + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.failure.summary).toBe('Failed to read Prisma schema at "./schema.prisma"'); + expect(result.failure.diagnostics).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: 'PSL_SCHEMA_READ_FAILED', + sourceId: './schema.prisma', + }), + ]), + ); + }); }); describe('given namespaced extension constructors in schema', () => { diff --git a/test/integration/test/cli-journeys/invalid-utf8-schema.e2e.test.ts b/test/integration/test/cli-journeys/invalid-utf8-schema.e2e.test.ts new file mode 100644 index 000000000000..a1c4b4713fac --- /dev/null +++ b/test/integration/test/cli-journeys/invalid-utf8-schema.e2e.test.ts @@ -0,0 +1,39 @@ +/** + * Invalid UTF-8 Schema Handling (Issue #30198) + * + * Verifies that contract emit fails with a non-zero exit code and explicit error + * diagnostics when contract.prisma contains invalid UTF-8 byte sequences (such as a + * lone Windows-1252 0x97 em-dash byte). + */ + +import { Buffer } from 'node:buffer'; +import { writeFileSync } from 'node:fs'; +import { join } from 'pathe'; +import { describe, expect, it } from 'vitest'; +import { withTempDir } from '../utils/cli-test-helpers'; +import { runContractEmit, setupJourney, timeouts } from '../utils/journey-test-helpers'; + +withTempDir(({ createTempDir }) => { + describe('Issue #30198: Invalid UTF-8 Schema Handling', () => { + it( + 'fails with exit code 1 and outputs error when schema contains non-UTF-8 bytes', + async () => { + const ctx = setupJourney({ createTempDir, contractMode: 'psl' }); + + const schemaPath = join(ctx.testDir, 'contract.prisma'); + const invalidSchemaContent = Buffer.concat([ + Buffer.from('model User {\n id Int @id // comment with CP-1252 em-dash: ', 'utf-8'), + Buffer.from([0x97]), + Buffer.from('\n}\n', 'utf-8'), + ]); + writeFileSync(schemaPath, invalidSchemaContent); + + const result = await runContractEmit(ctx, ['--json']); + + expect(result.exitCode).toBe(1); + expect(result.stderr).toMatch(/PSL_SCHEMA_READ_FAILED|invalid UTF-8|Failed to decode/i); + }, + timeouts.typeScriptCompilation, + ); + }); +});