Files
paseo/packages/protocol/src/binary-frames/diff.ts
T
Mohamed Boudra 0a9e678286 fix(diff): preserve BOM text and complete file replacements
Decode each string without consuming its leading BOM. File-type replacements emit multiple patch sections for one listed path, so retain all matching hunks. Assert the optional rename metadata in the existing commit-file journey.
2026-11-06 07:24:19 +01:00

361 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ParsedDiffFile } from "../messages.js";
// Opcodes 0x40–0x4f are reserved for diff revisions. Plugins own 0x20–0x2f.
export const DiffOpcode = { Begin: 0x40, Bodies: 0x41, End: 0x42 } as const;
export const DIFF_HIGHLIGHT_STYLES = [
null,
"keyword",
"comment",
"string",
"number",
"literal",
"function",
"definition",
"class",
"type",
"tag",
"attribute",
"property",
"variable",
"operator",
"punctuation",
"regexp",
"escape",
"meta",
"heading",
"link",
] as const;
const CHANGES = ["added", "modified", "deleted", "renamed"] as const;
const CONTENTS = ["text", "binary"] as const;
const KINDS = ["context", "add", "remove", "header"] as const;
const ERRORS = [
null,
"NOT_GIT_REPO",
"REF_NOT_FOUND",
"INVALID_COMPARE",
"TOO_MANY_SUBSCRIPTIONS",
"UNKNOWN",
] as const;
export interface DiffEntry {
path: string;
oldPath: string | null;
change: (typeof CHANGES)[number];
content: (typeof CONTENTS)[number];
additions: number;
deletions: number;
body: boolean;
}
export interface DiffRun {
style: string | null;
length: number;
}
export interface DiffStreamLine {
type: (typeof KINDS)[number];
content: string;
runs?: DiffRun[];
}
export interface DiffStreamHunk {
oldStart: number;
oldCount: number;
newStart: number;
newCount: number;
lines: DiffStreamLine[];
}
export interface DiffBody {
path: string;
hunks: DiffStreamHunk[];
}
export interface DiffBegin {
opcode: typeof DiffOpcode.Begin;
slot: number;
revision: number;
baseRevision: number;
baseSha: string;
headSha: string;
mergeBaseSha: string;
entries: DiffEntry[];
}
export interface DiffBodies {
opcode: typeof DiffOpcode.Bodies;
slot: number;
revision: number;
files: DiffBody[];
}
export interface DiffEnd {
opcode: typeof DiffOpcode.End;
slot: number;
revision: number;
diffTooLarge: boolean;
omitted: string[];
error: { code: NonNullable<(typeof ERRORS)[number]>; message: string } | null;
}
export type DiffFrame = DiffBegin | DiffBodies | DiffEnd;
export class InvalidDiffFrameError extends Error {}
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
class Writer {
private bytes: number[] = [];
byte(value: number): void {
if (!Number.isInteger(value) || value < 0 || value > 255)
throw new InvalidDiffFrameError("Invalid byte");
this.bytes.push(value);
}
uint(value: number): void {
if (!Number.isSafeInteger(value) || value < 0)
throw new InvalidDiffFrameError("Invalid unsigned integer");
do {
const next = value % 128;
value = Math.floor(value / 128);
this.byte(next + (value ? 128 : 0));
} while (value);
}
str(value: string): void {
const bytes = textEncoder.encode(value);
this.uint(bytes.length);
for (const byte of bytes) this.byte(byte);
}
finish(): Uint8Array {
return Uint8Array.from(this.bytes);
}
}
class Reader {
private offset = 0;
constructor(private bytes: Uint8Array) {}
byte(): number {
if (this.offset >= this.bytes.length) throw new InvalidDiffFrameError("Truncated diff frame");
return this.bytes[this.offset++];
}
uint(): number {
let value = 0;
let factor = 1;
for (let index = 0; index < 8; index++) {
const byte = this.byte();
value += (byte & 127) * factor;
if (!Number.isSafeInteger(value)) throw new InvalidDiffFrameError("Integer overflow");
if (byte < 128) return value;
factor *= 128;
}
throw new InvalidDiffFrameError("Invalid varint");
}
count(): number {
const value = this.uint();
if (value > this.bytes.length - this.offset)
throw new InvalidDiffFrameError("Count exceeds frame");
return value;
}
str(): string {
const size = this.count();
const start = this.offset;
this.offset += size;
try {
return textDecoder.decode(this.bytes.subarray(start, this.offset));
} catch {
throw new InvalidDiffFrameError("Invalid UTF-8");
}
}
done(): void {
if (this.offset !== this.bytes.length) throw new InvalidDiffFrameError("Trailing bytes");
}
}
function enumAt<T>(values: readonly T[], index: number): T {
if (index >= values.length) throw new InvalidDiffFrameError("Unknown enum value");
return values[index];
}
function validateRuns(line: DiffStreamLine): void {
if (!line.runs) return;
let length = 0;
for (const run of line.runs) {
if (!Number.isSafeInteger(run.length) || run.length <= 0)
throw new InvalidDiffFrameError("Invalid run length");
length += run.length;
}
if (length !== line.content.length) throw new InvalidDiffFrameError("Runs do not cover line");
}
function writeLine(writer: Writer, line: DiffStreamLine): void {
validateRuns(line);
writer.byte(KINDS.indexOf(line.type) | (line.runs ? 128 : 0));
writer.str(line.content);
writer.uint(line.runs ? line.runs.length : 0);
for (const run of line.runs ?? []) {
const style = DIFF_HIGHLIGHT_STYLES.findIndex((value) => value === run.style);
writer.byte(Math.max(0, style));
writer.uint(run.length);
}
}
function readLine(reader: Reader): DiffStreamLine {
const flags = reader.byte();
const type = enumAt(KINDS, flags & 127);
const content = reader.str();
const count = reader.count();
const runs: DiffRun[] = [];
for (let index = 0; index < count; index++) {
const styleIndex = reader.byte();
const style = DIFF_HIGHLIGHT_STYLES[styleIndex] ?? null;
runs.push({ style, length: reader.uint() });
}
if (!(flags & 128)) {
if (count) throw new InvalidDiffFrameError("Runs without flag");
return { type, content };
}
const line = { type, content, runs };
validateRuns(line);
return line;
}
export function encodeDiffFrame(frame: DiffFrame): Uint8Array {
const writer = new Writer();
writer.byte(frame.opcode);
writer.byte(frame.slot);
writer.byte(1);
writer.uint(frame.revision);
if (frame.opcode === DiffOpcode.Begin) {
writer.uint(frame.baseRevision);
writer.str(frame.baseSha);
writer.str(frame.headSha);
writer.str(frame.mergeBaseSha);
writer.uint(frame.entries.length);
for (const entry of frame.entries) {
if (entry.content === "binary" && entry.body)
throw new InvalidDiffFrameError("Binary entry has a body");
writer.str(entry.path);
writer.str(entry.oldPath ?? "");
writer.byte(CHANGES.indexOf(entry.change));
writer.byte(CONTENTS.indexOf(entry.content));
writer.uint(entry.additions);
writer.uint(entry.deletions);
writer.byte(Number(entry.body));
}
} else if (frame.opcode === DiffOpcode.Bodies) {
writer.uint(frame.files.length);
for (const file of frame.files) {
writer.str(file.path);
writer.uint(file.hunks.length);
for (const hunk of file.hunks) {
writer.uint(hunk.oldStart);
writer.uint(hunk.oldCount);
writer.uint(hunk.newStart);
writer.uint(hunk.newCount);
writer.uint(hunk.lines.length);
for (const line of hunk.lines) writeLine(writer, line);
}
}
} else {
writer.byte(Number(frame.diffTooLarge));
writer.uint(frame.omitted.length);
for (const path of frame.omitted) writer.str(path);
writer.byte(frame.error ? ERRORS.indexOf(frame.error.code) : 0);
writer.str(frame.error ? frame.error.message : "");
}
return writer.finish();
}
export function decodeDiffFrame(bytes: Uint8Array): DiffFrame {
const reader = new Reader(bytes);
const opcode = reader.byte();
const slot = reader.byte();
if (reader.byte() !== 1) throw new InvalidDiffFrameError("Unknown diff version");
const revision = reader.uint();
let frame: DiffFrame;
if (opcode === DiffOpcode.Begin) {
const baseRevision = reader.uint();
const baseSha = reader.str();
const headSha = reader.str();
const mergeBaseSha = reader.str();
const count = reader.count();
const entries: DiffEntry[] = [];
for (let index = 0; index < count; index++) {
const path = reader.str();
const oldPath = reader.str() || null;
const change = enumAt(CHANGES, reader.byte());
const content = enumAt(CONTENTS, reader.byte());
const additions = reader.uint();
const deletions = reader.uint();
const bodyByte = reader.byte();
if (bodyByte > 1 || (content === "binary" && bodyByte))
throw new InvalidDiffFrameError("Invalid entry body");
entries.push({ path, oldPath, change, content, additions, deletions, body: bodyByte === 1 });
}
frame = { opcode, slot, revision, baseRevision, baseSha, headSha, mergeBaseSha, entries };
} else if (opcode === DiffOpcode.Bodies) {
const count = reader.count();
const files: DiffBody[] = [];
for (let index = 0; index < count; index++) {
const path = reader.str();
const hunkCount = reader.count();
const hunks: DiffStreamHunk[] = [];
for (let h = 0; h < hunkCount; h++) {
const oldStart = reader.uint();
const oldCount = reader.uint();
const newStart = reader.uint();
const newCount = reader.uint();
const lineCount = reader.count();
const lines: DiffStreamLine[] = [];
for (let l = 0; l < lineCount; l++) lines.push(readLine(reader));
hunks.push({ oldStart, oldCount, newStart, newCount, lines });
}
files.push({ path, hunks });
}
frame = { opcode, slot, revision, files };
} else if (opcode === DiffOpcode.End) {
const flags = reader.byte();
if (flags > 1) throw new InvalidDiffFrameError("Unknown flags");
const count = reader.count();
const omitted: string[] = [];
for (let index = 0; index < count; index++) omitted.push(reader.str());
const code = enumAt(ERRORS, reader.byte());
const message = reader.str();
frame = {
opcode,
slot,
revision,
diffTooLarge: flags === 1,
omitted,
error: code ? { code, message } : null,
};
} else {
throw new InvalidDiffFrameError("Unknown diff opcode");
}
reader.done();
return frame;
}
export function runsToTokens(
line: DiffStreamLine,
): NonNullable<ParsedDiffFile["hunks"][number]["lines"][number]["tokens"]> {
let offset = 0;
return (line.runs ?? []).map((run) => {
const text = line.content.slice(offset, offset + run.length);
offset += run.length;
return { text, style: run.style };
});
}
export function materializeDiffFile(
entry: DiffEntry,
body: DiffBody | null,
omitted: boolean,
): ParsedDiffFile {
let status: "ok" | "binary" | "too_large" = "ok";
if (entry.content === "binary") status = "binary";
else if (omitted) status = "too_large";
const hunks = body
? body.hunks.map((hunk) => ({
...hunk,
lines: hunk.lines.map((line) => {
if (line.runs)
return { type: line.type, content: line.content, tokens: runsToTokens(line) };
return { type: line.type, content: line.content };
}),
}))
: [];
const file: ParsedDiffFile = {
path: entry.path,
isNew: entry.change === "added",
isDeleted: entry.change === "deleted",
additions: entry.additions,
deletions: entry.deletions,
hunks,
status,
};
if (entry.oldPath !== null) file.oldPath = entry.oldPath;
return file;
}