Files
reverse-engineering-anything/src/python/PythonPyc.fixture.ts
T

190 lines
5.7 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 { PythonPycCodeLayout } from "../domain/python/pythonPyc.js";
const FLAG_REF = 0x80;
const u8 = (value: number): Buffer => Buffer.from([value]);
const u32 = (value: number): Buffer => {
const bytes = Buffer.alloc(4);
bytes.writeUInt32LE(value);
return bytes;
};
const i32 = (value: number): Buffer => {
const bytes = Buffer.alloc(4);
bytes.writeInt32LE(value);
return bytes;
};
const concat = (...parts: Buffer[]): Buffer => Buffer.concat(parts);
/** 16-byte CPython 3.7+ pyc header. Hash-based pycs store an 8-byte source hash. */
export const pythonPycHeader = (
magic: number,
fields: {
flags?: number;
mtime?: number;
sourceSize?: number;
sourceHash?: Buffer;
} = {},
): Buffer => {
const header = Buffer.alloc(16);
header.writeUInt16LE(magic, 0);
header[2] = 0x0d;
header[3] = 0x0a;
const flags = fields.flags ?? 0;
header.writeUInt32LE(flags, 4);
if ((flags & 1) !== 0) {
const hash = fields.sourceHash ?? Buffer.alloc(8);
if (hash.length !== 8)
throw new Error("Hash-based pyc header requires 8 source-hash bytes.");
hash.copy(header, 8);
return header;
}
header.writeUInt32LE(fields.mtime ?? 0, 8);
header.writeUInt32LE(fields.sourceSize ?? 0, 12);
return header;
};
const marshalNone = (): Buffer => u8(0x4e);
const marshalBytes = (value: Buffer, flagged = false): Buffer =>
concat(u8(0x73 | (flagged ? FLAG_REF : 0)), u32(value.length), value);
const marshalShortAscii = (value: string, interned = false): Buffer =>
concat(
u8(interned ? 0x5a : 0x7a),
u8(Buffer.byteLength(value, "utf8")),
Buffer.from(value, "utf8"),
);
const marshalSmallTuple = (items: Buffer[]): Buffer =>
concat(u8(0x29), u8(items.length), ...items);
export type FixtureCodeFields = {
argcount?: number;
posonlyargcount?: number;
kwonlyargcount?: number;
nlocals?: number;
stacksize?: number;
flags?: number;
bytecode?: Buffer;
consts?: Buffer[];
names?: string[];
varnames?: string[];
freevars?: string[];
cellvars?: string[];
localsplusnames?: string[];
localspluskinds?: Buffer;
/** Raw marshal object in place of the co_consts tuple. */
constsObject?: Buffer;
filename?: string;
name?: string;
qualname?: string;
/** Raw marshal object in place of co_name. */
nameObject?: Buffer;
/** Raw marshal object in place of co_qualname. */
qualnameObject?: Buffer;
firstlineno?: number;
linetable?: Buffer;
exceptiontable?: Buffer;
/** Raw marshal object in place of the co_names tuple. */
namesObject?: Buffer;
/** Raw marshal object in place of the co_exceptiontable bytes. */
exceptiontableObject?: Buffer;
flagged?: boolean;
};
/** Marshal TYPE_CODE matching CPython 3.7, 3.8–3.10, or 3.11+ field order. */
export const marshalCode = (
layout: PythonPycCodeLayout,
fields: FixtureCodeFields = {},
): Buffer => {
const parts: Buffer[] = [
u8(0x63 | (fields.flagged === true ? FLAG_REF : 0)),
u32(fields.argcount ?? 0),
];
if (layout !== "py37") parts.push(u32(fields.posonlyargcount ?? 0));
parts.push(u32(fields.kwonlyargcount ?? 0));
if (layout !== "py311") parts.push(u32(fields.nlocals ?? 0));
parts.push(
u32(fields.stacksize ?? 1),
u32(fields.flags ?? 0),
marshalBytes(fields.bytecode ?? Buffer.from([0x53, 0x00])),
fields.constsObject ?? marshalSmallTuple(fields.consts ?? [marshalNone()]),
fields.namesObject ??
marshalSmallTuple(
(fields.names ?? []).map((name) => marshalShortAscii(name)),
),
);
if (layout === "py311") {
parts.push(
marshalSmallTuple(
(fields.localsplusnames ?? []).map((name) => marshalShortAscii(name)),
),
marshalBytes(fields.localspluskinds ?? Buffer.alloc(0)),
);
} else {
parts.push(
marshalSmallTuple(
(fields.varnames ?? []).map((name) => marshalShortAscii(name)),
),
marshalSmallTuple(
(fields.freevars ?? []).map((name) => marshalShortAscii(name)),
),
marshalSmallTuple(
(fields.cellvars ?? []).map((name) => marshalShortAscii(name)),
),
);
}
parts.push(
marshalShortAscii(fields.filename ?? "sample.py"),
fields.nameObject ?? marshalShortAscii(fields.name ?? "<module>"),
);
if (layout === "py311")
parts.push(
fields.qualnameObject ??
marshalShortAscii(fields.qualname ?? fields.name ?? "<module>"),
);
parts.push(
i32(fields.firstlineno ?? 1),
marshalBytes(fields.linetable ?? Buffer.alloc(0)),
);
if (layout === "py311")
parts.push(
fields.exceptiontableObject ??
marshalBytes(fields.exceptiontable ?? Buffer.alloc(0)),
);
return concat(...parts);
};
/** Complete pyc with a module code object and optional nested function. */
export const pythonPycFixture = (
layout: PythonPycCodeLayout = "py311",
options: {
magic?: number;
header?: Parameters<typeof pythonPycHeader>[1];
nested?: boolean;
flaggedCode?: boolean;
} = {},
): Buffer => {
const magic =
options.magic ??
(layout === "py37" ? 3394 : layout === "py38" ? 3439 : 3531);
const nested =
options.nested === true
? marshalCode(layout, {
name: "inner",
qualname: "inner",
filename: "sample.py",
firstlineno: 2,
bytecode: Buffer.from([0x53, 0x00]),
})
: undefined;
const module = marshalCode(layout, {
name: "<module>",
qualname: "<module>",
filename: "sample.py",
firstlineno: 1,
names: nested === undefined ? [] : ["inner"],
consts: nested === undefined ? [marshalNone()] : [nested, marshalNone()],
bytecode: Buffer.from([0x64, 0x00, 0x53, 0x00]),
...(options.flaggedCode === true ? { flagged: true } : {}),
});
return concat(pythonPycHeader(magic, options.header), module);
};