@peekling/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORS +8 -0
- package/LICENSE +202 -0
- package/LICENSING.md +20 -0
- package/NOTICE +6 -0
- package/README.md +264 -0
- package/dist/atomic-directory.d.ts +2 -0
- package/dist/atomic-directory.d.ts.map +1 -0
- package/dist/atomic-directory.js +28 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +100 -0
- package/dist/doctor.d.ts +12 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +121 -0
- package/dist/image.d.ts +4 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +7 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +569 -0
- package/dist/png.d.ts +7 -0
- package/dist/png.d.ts.map +1 -0
- package/dist/png.js +169 -0
- package/dist/zip.d.ts +2 -0
- package/dist/zip.d.ts.map +1 -0
- package/dist/zip.js +119 -0
- package/package.json +63 -0
package/dist/png.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { inspectImageStructure } from "@peekling/runtime/pack";
|
|
2
|
+
import { inflateSync } from "node:zlib";
|
|
3
|
+
import { PNG } from "pngjs";
|
|
4
|
+
const SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
5
|
+
const ADAM7_PASSES = [
|
|
6
|
+
[0, 0, 8, 8],
|
|
7
|
+
[4, 0, 8, 8],
|
|
8
|
+
[0, 4, 4, 8],
|
|
9
|
+
[2, 0, 4, 4],
|
|
10
|
+
[0, 2, 2, 4],
|
|
11
|
+
[1, 0, 2, 2],
|
|
12
|
+
[0, 1, 1, 2],
|
|
13
|
+
];
|
|
14
|
+
const COLOR_CHANNELS = {
|
|
15
|
+
0: 1,
|
|
16
|
+
2: 3,
|
|
17
|
+
3: 1,
|
|
18
|
+
4: 2,
|
|
19
|
+
6: 4,
|
|
20
|
+
};
|
|
21
|
+
function crc32(buffer) {
|
|
22
|
+
let crc = 0xffffffff;
|
|
23
|
+
for (const byte of buffer) {
|
|
24
|
+
crc ^= byte;
|
|
25
|
+
for (let bit = 0; bit < 8; bit++)
|
|
26
|
+
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
|
27
|
+
}
|
|
28
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
29
|
+
}
|
|
30
|
+
function chunk(type, data) {
|
|
31
|
+
const name = Buffer.from(type, "ascii");
|
|
32
|
+
const output = Buffer.alloc(12 + data.length);
|
|
33
|
+
output.writeUInt32BE(data.length, 0);
|
|
34
|
+
name.copy(output, 4);
|
|
35
|
+
data.copy(output, 8);
|
|
36
|
+
output.writeUInt32BE(crc32(Buffer.concat([name, data])), 8 + data.length);
|
|
37
|
+
return output;
|
|
38
|
+
}
|
|
39
|
+
export function createFixtureAtlas(rows = 3) {
|
|
40
|
+
const width = 512;
|
|
41
|
+
const height = rows * 32;
|
|
42
|
+
const stride = width / 4 + 1;
|
|
43
|
+
const pixels = Buffer.alloc(stride * height);
|
|
44
|
+
const pixel = (x, y, value) => {
|
|
45
|
+
const offset = y * stride + 1 + (x >> 2);
|
|
46
|
+
pixels[offset] = pixels[offset] | (value << (6 - (x & 3) * 2));
|
|
47
|
+
};
|
|
48
|
+
const usedCells = Math.min(rows * 16, 48);
|
|
49
|
+
for (let cell = 0; cell < usedCells; cell++) {
|
|
50
|
+
const originX = (cell % 16) * 32;
|
|
51
|
+
const originY = Math.floor(cell / 16) * 32;
|
|
52
|
+
for (let y = 8; y < 24; y++) {
|
|
53
|
+
for (let x = 8; x < 24; x++) {
|
|
54
|
+
const edge = x < 10 || x > 21 || y < 10 || y > 21;
|
|
55
|
+
if (!edge && !((x + y + cell) % 11 === 0))
|
|
56
|
+
continue;
|
|
57
|
+
pixel(originX + x, originY + y, 1 + (cell % 2));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const markerX = 11 + (cell % 2) * 8;
|
|
61
|
+
const markerY = 13 + (Math.floor(cell / 2) % 2) * 6;
|
|
62
|
+
for (let y = markerY; y < markerY + 3; y++) {
|
|
63
|
+
for (let x = markerX; x < markerX + 3; x++) {
|
|
64
|
+
pixel(originX + x, originY + y, 3);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const header = Buffer.alloc(13);
|
|
69
|
+
header.writeUInt32BE(width, 0);
|
|
70
|
+
header.writeUInt32BE(height, 4);
|
|
71
|
+
header.set([2, 3, 0, 0, 0], 8);
|
|
72
|
+
return Buffer.concat([
|
|
73
|
+
SIGNATURE,
|
|
74
|
+
chunk("IHDR", header),
|
|
75
|
+
chunk("PLTE", Buffer.from([0, 0, 0, 74, 144, 226, 226, 130, 74, 255, 255, 255])),
|
|
76
|
+
chunk("tRNS", Buffer.from([0, 255, 255, 255])),
|
|
77
|
+
chunk("IDAT", storedDeflate(pixels)),
|
|
78
|
+
chunk("IEND", Buffer.alloc(0)),
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
export function inspectPng(buffer) {
|
|
82
|
+
const structure = inspectImageStructure(buffer, "image/png");
|
|
83
|
+
const imageData = [];
|
|
84
|
+
let cursor = 8;
|
|
85
|
+
while (cursor + 12 <= buffer.length) {
|
|
86
|
+
const length = buffer.readUInt32BE(cursor);
|
|
87
|
+
const end = cursor + 12 + length;
|
|
88
|
+
const type = buffer.toString("ascii", cursor + 4, cursor + 8);
|
|
89
|
+
const expectedCrc = buffer.readUInt32BE(end - 4);
|
|
90
|
+
const actualCrc = crc32(buffer.subarray(cursor + 4, end - 4));
|
|
91
|
+
if (actualCrc !== expectedCrc)
|
|
92
|
+
throw new Error(`PNG ${type} chunk CRC is invalid`);
|
|
93
|
+
if (type === "IDAT")
|
|
94
|
+
imageData.push(buffer.subarray(cursor + 8, end - 4));
|
|
95
|
+
cursor = end;
|
|
96
|
+
if (type === "IEND")
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
const expectedInflatedBytes = inflatedPngByteLength(buffer, structure.width, structure.height);
|
|
100
|
+
const compressedBytes = Buffer.concat(imageData);
|
|
101
|
+
let inflatedBytes;
|
|
102
|
+
let consumedBytes;
|
|
103
|
+
try {
|
|
104
|
+
const result = inflateSync(compressedBytes, {
|
|
105
|
+
info: true,
|
|
106
|
+
maxOutputLength: expectedInflatedBytes,
|
|
107
|
+
});
|
|
108
|
+
inflatedBytes = result.buffer;
|
|
109
|
+
consumedBytes = result.engine.bytesWritten;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
throw new Error("atlas is not a decodable PNG file");
|
|
113
|
+
}
|
|
114
|
+
if (inflatedBytes.length !== expectedInflatedBytes ||
|
|
115
|
+
consumedBytes !== compressedBytes.length)
|
|
116
|
+
throw new Error("atlas is not a decodable PNG file");
|
|
117
|
+
let decoded;
|
|
118
|
+
try {
|
|
119
|
+
decoded = PNG.sync.read(buffer, { checkCRC: true });
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new Error("atlas is not a decodable PNG file");
|
|
123
|
+
}
|
|
124
|
+
if (decoded.width !== structure.width ||
|
|
125
|
+
decoded.height !== structure.height ||
|
|
126
|
+
decoded.data.length !== structure.width * structure.height * 4)
|
|
127
|
+
throw new Error("decoded PNG geometry does not match IHDR");
|
|
128
|
+
return {
|
|
129
|
+
width: structure.width,
|
|
130
|
+
height: structure.height,
|
|
131
|
+
hasAlpha: structure.hasAlpha,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function inflatedPngByteLength(buffer, width, height) {
|
|
135
|
+
const bitsPerPixel = COLOR_CHANNELS[buffer[25]] * buffer[24];
|
|
136
|
+
if (buffer[28] === 0)
|
|
137
|
+
return filteredPassByteLength(width, height, bitsPerPixel);
|
|
138
|
+
return ADAM7_PASSES.reduce((total, [startX, startY, stepX, stepY]) => total +
|
|
139
|
+
filteredPassByteLength(passLength(width, startX, stepX), passLength(height, startY, stepY), bitsPerPixel), 0);
|
|
140
|
+
}
|
|
141
|
+
function passLength(size, start, step) {
|
|
142
|
+
return size <= start ? 0 : Math.ceil((size - start) / step);
|
|
143
|
+
}
|
|
144
|
+
function filteredPassByteLength(width, height, bitsPerPixel) {
|
|
145
|
+
return width === 0 || height === 0
|
|
146
|
+
? 0
|
|
147
|
+
: height * (Math.ceil((width * bitsPerPixel) / 8) + 1);
|
|
148
|
+
}
|
|
149
|
+
function storedDeflate(data) {
|
|
150
|
+
const parts = [Buffer.from([0x78, 0x01])];
|
|
151
|
+
for (let offset = 0; offset < data.length; offset += 65_535) {
|
|
152
|
+
const length = Math.min(65_535, data.length - offset);
|
|
153
|
+
const header = Buffer.alloc(5);
|
|
154
|
+
header[0] = offset + length === data.length ? 1 : 0;
|
|
155
|
+
header.writeUInt16LE(length, 1);
|
|
156
|
+
header.writeUInt16LE(~length & 0xffff, 3);
|
|
157
|
+
parts.push(header, data.subarray(offset, offset + length));
|
|
158
|
+
}
|
|
159
|
+
let a = 1;
|
|
160
|
+
let b = 0;
|
|
161
|
+
for (const byte of data) {
|
|
162
|
+
a = (a + byte) % 65_521;
|
|
163
|
+
b = (b + a) % 65_521;
|
|
164
|
+
}
|
|
165
|
+
const checksum = Buffer.alloc(4);
|
|
166
|
+
checksum.writeUInt32BE(((b << 16) | a) >>> 0);
|
|
167
|
+
parts.push(checksum);
|
|
168
|
+
return Buffer.concat(parts);
|
|
169
|
+
}
|
package/dist/zip.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zip.d.ts","sourceRoot":"","sources":["../src/zip.ts"],"names":[],"mappings":"AAkBA,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAkHnE"}
|
package/dist/zip.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { inflateRawSync } from "node:zlib";
|
|
2
|
+
const MAX_ENTRIES = 128;
|
|
3
|
+
const MAX_TOTAL = 64 * 1024 * 1024;
|
|
4
|
+
function safeEntry(name) {
|
|
5
|
+
return (name.length > 0 &&
|
|
6
|
+
name.length <= 256 &&
|
|
7
|
+
!name.startsWith("/") &&
|
|
8
|
+
!name.includes("\\") &&
|
|
9
|
+
!/[\u0000-\u001f\u007f:]/.test(name) &&
|
|
10
|
+
name
|
|
11
|
+
.split("/")
|
|
12
|
+
.every((part) => part !== "" && part !== "." && part !== ".."));
|
|
13
|
+
}
|
|
14
|
+
export function readZipEntries(archive) {
|
|
15
|
+
if (archive.length < 22)
|
|
16
|
+
throw new Error(".codex-pet is not a supported ZIP archive");
|
|
17
|
+
let eocd = -1;
|
|
18
|
+
for (let offset = archive.length - 22; offset >= Math.max(0, archive.length - 65_557); offset--) {
|
|
19
|
+
if (archive.readUInt32LE(offset) === 0x06054b50) {
|
|
20
|
+
eocd = offset;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (eocd < 0)
|
|
25
|
+
throw new Error(".codex-pet is not a supported ZIP archive");
|
|
26
|
+
if (archive.readUInt16LE(eocd + 4) !== 0 ||
|
|
27
|
+
archive.readUInt16LE(eocd + 6) !== 0 ||
|
|
28
|
+
archive.readUInt16LE(eocd + 8) !== archive.readUInt16LE(eocd + 10))
|
|
29
|
+
throw new Error("Multi-disk and ZIP64 .codex-pet archives are unsupported");
|
|
30
|
+
const entries = archive.readUInt16LE(eocd + 10);
|
|
31
|
+
const directorySize = archive.readUInt32LE(eocd + 12);
|
|
32
|
+
let offset = archive.readUInt32LE(eocd + 16);
|
|
33
|
+
if (entries > MAX_ENTRIES ||
|
|
34
|
+
offset + directorySize > eocd ||
|
|
35
|
+
offset + directorySize > archive.length) {
|
|
36
|
+
throw new Error(".codex-pet archive exceeds safe directory limits");
|
|
37
|
+
}
|
|
38
|
+
const result = new Map();
|
|
39
|
+
let total = 0;
|
|
40
|
+
for (let index = 0; index < entries; index++) {
|
|
41
|
+
if (offset + 46 > archive.length)
|
|
42
|
+
throw new Error("ZIP directory is truncated");
|
|
43
|
+
if (archive.readUInt32LE(offset) !== 0x02014b50)
|
|
44
|
+
throw new Error("ZIP directory is malformed");
|
|
45
|
+
const flags = archive.readUInt16LE(offset + 8);
|
|
46
|
+
const method = archive.readUInt16LE(offset + 10);
|
|
47
|
+
const compressed = archive.readUInt32LE(offset + 20);
|
|
48
|
+
const uncompressed = archive.readUInt32LE(offset + 24);
|
|
49
|
+
const nameLength = archive.readUInt16LE(offset + 28);
|
|
50
|
+
const extraLength = archive.readUInt16LE(offset + 30);
|
|
51
|
+
const commentLength = archive.readUInt16LE(offset + 32);
|
|
52
|
+
const localOffset = archive.readUInt32LE(offset + 42);
|
|
53
|
+
const next = offset + 46 + nameLength + extraLength + commentLength;
|
|
54
|
+
if (next > archive.length)
|
|
55
|
+
throw new Error("ZIP directory is truncated");
|
|
56
|
+
let name;
|
|
57
|
+
try {
|
|
58
|
+
name = new TextDecoder("utf-8", { fatal: true }).decode(archive.subarray(offset + 46, offset + 46 + nameLength));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new Error("ZIP entry name is not valid UTF-8");
|
|
62
|
+
}
|
|
63
|
+
offset = next;
|
|
64
|
+
if (name.endsWith("/"))
|
|
65
|
+
continue;
|
|
66
|
+
if (!safeEntry(name))
|
|
67
|
+
throw new Error(`Unsafe ZIP entry path: ${name}`);
|
|
68
|
+
if ((flags & 1) !== 0)
|
|
69
|
+
throw new Error(`Encrypted ZIP entry is unsupported: ${name}`);
|
|
70
|
+
if (result.has(name))
|
|
71
|
+
throw new Error(`Duplicate ZIP entry: ${name}`);
|
|
72
|
+
if (uncompressed > 32 * 1024 * 1024 ||
|
|
73
|
+
(total += uncompressed) > MAX_TOTAL) {
|
|
74
|
+
throw new Error(".codex-pet archive exceeds safe uncompressed size limits");
|
|
75
|
+
}
|
|
76
|
+
if (localOffset + 30 > archive.length ||
|
|
77
|
+
archive.readUInt32LE(localOffset) !== 0x04034b50)
|
|
78
|
+
throw new Error("ZIP local header is malformed");
|
|
79
|
+
if (archive.readUInt16LE(localOffset + 8) !== method ||
|
|
80
|
+
archive.readUInt16LE(localOffset + 6) !== flags)
|
|
81
|
+
throw new Error(`ZIP headers disagree for ${name}`);
|
|
82
|
+
const localName = archive.readUInt16LE(localOffset + 26);
|
|
83
|
+
const localExtra = archive.readUInt16LE(localOffset + 28);
|
|
84
|
+
const start = localOffset + 30 + localName + localExtra;
|
|
85
|
+
if (start + compressed > archive.length || start + compressed > eocd)
|
|
86
|
+
throw new Error(`ZIP entry data is truncated: ${name}`);
|
|
87
|
+
const localPath = new TextDecoder("utf-8", { fatal: true }).decode(archive.subarray(localOffset + 30, localOffset + 30 + localName));
|
|
88
|
+
if (localPath !== name)
|
|
89
|
+
throw new Error(`ZIP headers disagree for ${name}`);
|
|
90
|
+
if (compressed > 0 &&
|
|
91
|
+
uncompressed > 1024 * 1024 &&
|
|
92
|
+
uncompressed / compressed > 1000)
|
|
93
|
+
throw new Error(`ZIP compression ratio is unsafe for ${name}`);
|
|
94
|
+
const bytes = archive.subarray(start, start + compressed);
|
|
95
|
+
let value;
|
|
96
|
+
if (method === 0)
|
|
97
|
+
value = Buffer.from(bytes);
|
|
98
|
+
else if (method === 8)
|
|
99
|
+
value = inflateRawSync(bytes, { maxOutputLength: 32 * 1024 * 1024 });
|
|
100
|
+
else
|
|
101
|
+
throw new Error(`Unsupported ZIP compression method for ${name}`);
|
|
102
|
+
if (value.length !== uncompressed)
|
|
103
|
+
throw new Error(`ZIP size mismatch for ${name}`);
|
|
104
|
+
if (crc32(value) !==
|
|
105
|
+
archive.readUInt32LE(offset - commentLength - extraLength - nameLength - 46 + 16))
|
|
106
|
+
throw new Error(`ZIP checksum mismatch for ${name}`);
|
|
107
|
+
result.set(name, value);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
function crc32(bytes) {
|
|
112
|
+
let crc = 0xffffffff;
|
|
113
|
+
for (const byte of bytes) {
|
|
114
|
+
crc ^= byte;
|
|
115
|
+
for (let bit = 0; bit < 8; bit++)
|
|
116
|
+
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
|
|
117
|
+
}
|
|
118
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
119
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@peekling/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Inspect Peekling configuration and author data-only character packs",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Prajwal S. Venkateshmurthy",
|
|
8
|
+
"url": "https://prajwal.me"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/peekling/peekling-engine.git"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/peekling/peekling-engine#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/peekling/peekling-engine/issues"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=22.14.0"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"provenance": true
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist/**/*.js",
|
|
28
|
+
"dist/**/*.d.ts",
|
|
29
|
+
"dist/**/*.d.ts.map",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"NOTICE",
|
|
33
|
+
"AUTHORS",
|
|
34
|
+
"LICENSING.md"
|
|
35
|
+
],
|
|
36
|
+
"bin": {
|
|
37
|
+
"peekling": "./dist/bin.js"
|
|
38
|
+
},
|
|
39
|
+
"main": "./dist/index.js",
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"exports": {
|
|
42
|
+
".": {
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"import": "./dist/index.js"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@peekling/adapter-codex-pet": "0.1.0",
|
|
49
|
+
"@peekling/preflight": "0.1.0",
|
|
50
|
+
"@peekling/runtime": "0.1.0",
|
|
51
|
+
"pngjs": "^7.0.0"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsc -b",
|
|
55
|
+
"prepack": "npm --prefix ../.. run build",
|
|
56
|
+
"test": "npm run build && node --test --experimental-strip-types test/*.test.ts",
|
|
57
|
+
"typecheck": "tsc -b --pretty false"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "^24.3.0",
|
|
61
|
+
"typescript": "^7.0.2"
|
|
62
|
+
}
|
|
63
|
+
}
|