Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions dist/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const run_1 = require("../sandbox/run");
const guardrails_1 = require("../lib/guardrails");
const api_url_1 = require("../lib/api-url");
const install_key_1 = require("../lib/install-key");
const decode_text_1 = require("../lib/decode-text");
const owner_receipt_1 = require("../lib/owner-receipt");
/** Exit codes — a small, stable contract so CI scripts can branch on them. */
exports.EXIT = {
Expand Down Expand Up @@ -1373,7 +1374,7 @@ async function run(args, client, io = realIO) {
const file = args.operand;
let raw;
try {
raw = (0, node_fs_1.readFileSync)(file, 'utf8');
raw = (0, decode_text_1.decodeText)((0, node_fs_1.readFileSync)(file));
}
catch (e) {
io.err(`verify-receipt: cannot read ${file}: ${e.message}`);
Expand All @@ -1399,7 +1400,7 @@ async function run(args, client, io = realIO) {
let envelope = obj && typeof obj === 'object' ? obj['six_claims'] : undefined;
if (args.envelope) {
try {
envelope = JSON.parse((0, node_fs_1.readFileSync)(args.envelope, 'utf8'));
envelope = JSON.parse((0, decode_text_1.decodeText)((0, node_fs_1.readFileSync)(args.envelope)));
}
catch (e) {
io.err(`verify-receipt: cannot read --envelope ${args.envelope}: ${e.message}`);
Expand All @@ -1426,7 +1427,7 @@ async function run(args, client, io = realIO) {
const file = args.operand;
let raw;
try {
raw = (0, node_fs_1.readFileSync)(file === '-' ? 0 : file, 'utf8'); // fd 0 = stdin
raw = (0, decode_text_1.decodeText)((0, node_fs_1.readFileSync)(file === '-' ? 0 : file)); // fd 0 = stdin
}
catch (e) {
io.err(`sign-receipt: cannot read ${file === '-' ? 'stdin' : file}: ${e.message}`);
Expand Down Expand Up @@ -1683,7 +1684,7 @@ async function run(args, client, io = realIO) {
return exports.EXIT.USAGE;
}
try {
evidence = JSON.parse(fs.readFileSync(args.evidence, 'utf8'));
evidence = JSON.parse((0, decode_text_1.decodeText)(fs.readFileSync(args.evidence)));
}
catch {
io.err(`report: evidence file is not valid JSON: ${args.evidence}`);
Expand Down Expand Up @@ -1731,11 +1732,9 @@ function readClaimsFile(path, fs = require('node:fs')) {
throw new Error(`--claims-file: no such file: ${path}`);
if (st.size > action_evidence_1.MAX_CLAIMS_FILE_BYTES)
throw new Error(`--claims-file is ${st.size} bytes; the limit is ${action_evidence_1.MAX_CLAIMS_FILE_BYTES}`);
const text = String(fs.readFileSync(path, 'utf8'));
// One leading byte-order mark is a file-encoding marker, not text: Windows PowerShell 5.1 writes it
// for `Out-File` and `Set-Content -Encoding utf8`, and kept it would make line 1 non-ASCII and
// block every verdict (XC1 re-check 5). Only that one character is dropped; any other stays.
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
// Windows PowerShell 5.1 writes a byte-order mark for `Out-File` and `Set-Content -Encoding utf8`, and UTF-16 for
// `>`. A mark kept in the text would make line 1 non-ASCII and block every verdict (XC1 re-check 5).
return (0, decode_text_1.decodeText)(fs.readFileSync(path));
}
/**
* `trustshell check <runUrl> --claims "<text>"` (or `--claims-file <path>`). GitHub only, exactly like the URL form: the
Expand Down
6 changes: 6 additions & 0 deletions dist/lib/decode-text.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Text a person saved on any platform. Windows PowerShell 5.1's `>` writes UTF-16 with a byte-order mark, so
* `trustshell sign-receipt body.json > r.json`, the documented command, produced a file `verify-receipt` called
* "not valid JSON". A byte-order mark says how the bytes are encoded and is never part of the text.
*/
export declare function decodeText(raw: Buffer | string): string;
18 changes: 18 additions & 0 deletions dist/lib/decode-text.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.decodeText = decodeText;
/**
* Text a person saved on any platform. Windows PowerShell 5.1's `>` writes UTF-16 with a byte-order mark, so
* `trustshell sign-receipt body.json > r.json`, the documented command, produced a file `verify-receipt` called
* "not valid JSON". A byte-order mark says how the bytes are encoded and is never part of the text.
*/
function decodeText(raw) {
if (typeof raw === 'string')
return raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
if (raw[0] === 0xff && raw[1] === 0xfe)
return raw.subarray(2).toString('utf16le');
if (raw[0] === 0xfe && raw[1] === 0xff)
return Buffer.from(raw.subarray(2)).swap16().toString('utf16le');
const text = raw.toString('utf8');
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
}
17 changes: 8 additions & 9 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import { sandboxCli } from '../sandbox/run';
import { GUARD_RECORD_SCHEMA, trustshellHome } from '../lib/guardrails';
import { ApiUrlError } from '../lib/api-url';
import { signingKey } from '../lib/install-key';
import { decodeText } from '../lib/decode-text';
import { signOwnerReceipt, installKeyRootHex, type SealableReceipt } from '../lib/owner-receipt';

/** Exit codes — a small, stable contract so CI scripts can branch on them. */
Expand Down Expand Up @@ -1454,7 +1455,7 @@ export async function run(
const file = args.operand as string;
let raw: string;
try {
raw = readTextFile(file, 'utf8');
raw = decodeText(readTextFile(file));
} catch (e) {
io.err(`verify-receipt: cannot read ${file}: ${(e as Error).message}`);
return EXIT.USAGE;
Expand All @@ -1477,7 +1478,7 @@ export async function run(
let envelope: unknown = obj && typeof obj === 'object' ? obj['six_claims'] : undefined;
if (args.envelope) {
try {
envelope = JSON.parse(readTextFile(args.envelope, 'utf8'));
envelope = JSON.parse(decodeText(readTextFile(args.envelope)));
} catch (e) {
io.err(`verify-receipt: cannot read --envelope ${args.envelope}: ${(e as Error).message}`);
return EXIT.USAGE;
Expand All @@ -1502,7 +1503,7 @@ export async function run(
const file = args.operand as string;
let raw: string;
try {
raw = readTextFile(file === '-' ? 0 : file, 'utf8'); // fd 0 = stdin
raw = decodeText(readTextFile(file === '-' ? 0 : file)); // fd 0 = stdin
} catch (e) {
io.err(`sign-receipt: cannot read ${file === '-' ? 'stdin' : file}: ${(e as Error).message}`);
return EXIT.USAGE;
Expand Down Expand Up @@ -1746,7 +1747,7 @@ export async function run(
return EXIT.USAGE;
}
try {
evidence = JSON.parse(fs.readFileSync(args.evidence, 'utf8')) as EvidenceDoc;
evidence = JSON.parse(decodeText(fs.readFileSync(args.evidence))) as EvidenceDoc;
} catch {
io.err(`report: evidence file is not valid JSON: ${args.evidence}`);
return EXIT.USAGE;
Expand Down Expand Up @@ -1796,11 +1797,9 @@ export function readClaimsFile(
}
if (!st?.isFile()) throw new Error(`--claims-file: no such file: ${path}`);
if (st.size > MAX_CLAIMS_FILE_BYTES) throw new Error(`--claims-file is ${st.size} bytes; the limit is ${MAX_CLAIMS_FILE_BYTES}`);
const text = String(fs.readFileSync(path, 'utf8'));
// One leading byte-order mark is a file-encoding marker, not text: Windows PowerShell 5.1 writes it
// for `Out-File` and `Set-Content -Encoding utf8`, and kept it would make line 1 non-ASCII and
// block every verdict (XC1 re-check 5). Only that one character is dropped; any other stays.
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
// Windows PowerShell 5.1 writes a byte-order mark for `Out-File` and `Set-Content -Encoding utf8`, and UTF-16 for
// `>`. A mark kept in the text would make line 1 non-ASCII and block every verdict (XC1 re-check 5).
return decodeText(fs.readFileSync(path));
}

/**
Expand Down
12 changes: 12 additions & 0 deletions src/lib/decode-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Text a person saved on any platform. Windows PowerShell 5.1's `>` writes UTF-16 with a byte-order mark, so
* `trustshell sign-receipt body.json > r.json`, the documented command, produced a file `verify-receipt` called
* "not valid JSON". A byte-order mark says how the bytes are encoded and is never part of the text.
*/
export function decodeText(raw: Buffer | string): string {
if (typeof raw === 'string') return raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
if (raw[0] === 0xff && raw[1] === 0xfe) return raw.subarray(2).toString('utf16le');
if (raw[0] === 0xfe && raw[1] === 0xff) return Buffer.from(raw.subarray(2)).swap16().toString('utf16le');
const text = raw.toString('utf8');
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
}
18 changes: 18 additions & 0 deletions tests/decode-text.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { decodeText } from '../src/lib/decode-text';

describe('decodeText', () => {
const text = '{"a":"é"}\n';
it('reads what Windows PowerShell 5.1 > writes', () => {
expect(decodeText(Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(text, 'utf16le')]))).toBe(text);
});
it('reads UTF-16 big-endian', () => {
expect(decodeText(Buffer.concat([Buffer.from([0xfe, 0xff]), Buffer.from(text, 'utf16le').swap16()]))).toBe(text);
});
it('drops one UTF-8 byte-order mark and keeps the rest', () => {
expect(decodeText(Buffer.from(`${text}`, 'utf8'))).toBe(text);
expect(decodeText(`${text}`)).toBe(`${text}`);
});
it('leaves plain UTF-8 alone', () => {
expect(decodeText(Buffer.from(text, 'utf8'))).toBe(text);
});
});
Loading