Skip to content

Commit 98307b5

Browse files
committed
feat: fix hardcoded frame & bug
1 parent f6a354d commit 98307b5

6 files changed

Lines changed: 124 additions & 41 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "logo-bin-tool",
3-
"version": "1.0.0",
3+
"version": "1.0.1",
44
"description": "Web-based extract and repack tool for Unisoc logo.bin firmware images",
55
"main": "server.js",
66
"scripts": {

public/js/app.js

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -190,34 +190,37 @@ async function downloadFrame(index) {
190190
}
191191

192192
async function handleExtractAll() {
193-
log("Extracting all frames as PNG...", "info");
194-
195-
for (const frame of frames) {
196-
canvas.width = frame.width;
197-
canvas.height = frame.height;
198-
const ctx = canvas.getContext("2d");
199-
200-
const img = await new Promise((resolve) => {
201-
const i = new Image();
202-
i.onload = () => resolve(i);
203-
i.src = frame.url;
193+
log(`Extracting all ${frames.length} frames as PNG...`, "info");
194+
195+
const renderFrameToBlob = (frame) =>
196+
new Promise((resolve, reject) => {
197+
const offscreen = document.createElement("canvas");
198+
offscreen.width = frame.width;
199+
offscreen.height = frame.height;
200+
const ctx = offscreen.getContext("2d");
201+
202+
const img = new Image();
203+
img.onload = () => {
204+
ctx.drawImage(img, 0, 0);
205+
offscreen.toBlob((blob) => {
206+
if (!blob) return reject(new Error(`Failed to render frame ${frame.index}`));
207+
resolve(blob);
208+
}, "image/png");
209+
};
210+
img.onerror = () => reject(new Error(`Failed to load frame ${frame.index}`));
211+
img.src = frame.url;
204212
});
205213

206-
ctx.drawImage(img, 0, 0);
207-
208-
await new Promise((resolve) => {
209-
canvas.toBlob((blob) => {
210-
const a = document.createElement("a");
211-
a.href = URL.createObjectURL(blob);
212-
a.download = `frame_${frame.index}.png`;
213-
a.click();
214-
URL.revokeObjectURL(a.href);
215-
log(`Extracted frame_${frame.index}.png`, "ok");
216-
resolve();
217-
}, "image/png");
218-
});
214+
const blobs = await Promise.all(frames.map(renderFrameToBlob));
219215

220-
await new Promise((r) => setTimeout(r, 250));
216+
for (let i = 0; i < blobs.length; i++) {
217+
const a = document.createElement("a");
218+
a.href = URL.createObjectURL(blobs[i]);
219+
a.download = `frame_${frames[i].index}.png`;
220+
a.click();
221+
URL.revokeObjectURL(a.href);
222+
log(`Extracted frame_${frames[i].index}.png`, "ok");
223+
if (i < blobs.length - 1) await new Promise((r) => setTimeout(r, 80));
221224
}
222225
}
223226

public/js/bmp.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
function imageToBmp(imageData, width, height, targetFileSize) {
22
const rowSize = Math.ceil((width * 3) / 4) * 4;
33
const pixelDataSize = rowSize * height;
4-
const fileSize = targetFileSize || 54 + pixelDataSize;
4+
const minFileSize = 54 + pixelDataSize;
55

6+
if (targetFileSize && targetFileSize < minFileSize) {
7+
throw new Error(
8+
`targetFileSize (${targetFileSize}) is smaller than the minimum required ` +
9+
`BMP size (${minFileSize}) for a ${width}×${height} 24bpp image. ` +
10+
`The original logo.bin may be corrupt or from an unsupported variant.`
11+
);
12+
}
13+
14+
const fileSize = targetFileSize || minFileSize;
615
const buffer = new ArrayBuffer(fileSize);
716
const bytes = new Uint8Array(buffer);
817
const view = new DataView(buffer);

public/js/parser.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
const HEADER_SIZE = 44;
21
const GZ_MAGIC = [0x47, 0x5a];
2+
const SIZES_OFFSET = 24;
3+
const SIZE_ENTRY = 4;
4+
5+
function computeHeaderSize(frameCount) {
6+
return SIZES_OFFSET + frameCount * SIZE_ENTRY;
7+
}
38

49
function validateMagic(bytes) {
510
return bytes[0] === GZ_MAGIC[0] && bytes[1] === GZ_MAGIC[1];
@@ -8,7 +13,7 @@ function validateMagic(bytes) {
813
function readSizes(view, count) {
914
const sizes = [];
1015
for (let i = 0; i < count; i++) {
11-
sizes.push(view.getUint32(24 + i * 4, true));
16+
sizes.push(view.getUint32(SIZES_OFFSET + i * SIZE_ENTRY, true));
1217
}
1318
return sizes;
1419
}
@@ -17,9 +22,18 @@ async function parseLogoBin(arrayBuffer) {
1722
const bytes = new Uint8Array(arrayBuffer);
1823
const view = new DataView(arrayBuffer);
1924

20-
if (!validateMagic(bytes)) throw new Error("Invalid file: expected GZ magic bytes");
25+
if (!validateMagic(bytes)) throw new Error("Invalid file: expected GZ magic bytes (0x47 0x5A)");
2126

2227
const frameCount = view.getUint32(2, true);
28+
if (frameCount === 0 || frameCount > 64) {
29+
throw new Error(`Unexpected frame count: ${frameCount}. File may be corrupt or unsupported.`);
30+
}
31+
32+
const HEADER_SIZE = computeHeaderSize(frameCount);
33+
if (arrayBuffer.byteLength < HEADER_SIZE) {
34+
throw new Error("File too small to contain a valid header.");
35+
}
36+
2337
const sizes = readSizes(view, frameCount);
2438

2539
let offset = HEADER_SIZE;
@@ -56,6 +70,7 @@ async function buildLogoBin(frames) {
5670

5771
const sizes = recompressed.map((c) => c.length);
5872
const totalPayload = sizes.reduce((a, b) => a + b, 0);
73+
const HEADER_SIZE = computeHeaderSize(frames.length);
5974
const output = new Uint8Array(HEADER_SIZE + totalPayload);
6075
const outView = new DataView(output.buffer);
6176

@@ -64,7 +79,7 @@ async function buildLogoBin(frames) {
6479
outView.setUint32(2, frames.length, true);
6580

6681
for (let i = 0; i < frames.length; i++) {
67-
outView.setUint32(24 + i * 4, sizes[i], true);
82+
outView.setUint32(SIZES_OFFSET + i * SIZE_ENTRY, sizes[i], true);
6883
}
6984

7085
let writeOffset = HEADER_SIZE;

src/routes/api.js

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,40 @@ const upload = multer({
99
limits: { fileSize: 50 * 1024 * 1024 },
1010
});
1111

12-
router.post("/extract", upload.single("bin"), async (req, res) => {
12+
const rateLimitMap = new Map();
13+
const RATE_LIMIT = 30;
14+
const RATE_WINDOW = 60 * 1000;
15+
16+
function rateLimit(req, res, next) {
17+
const ip = req.ip || req.socket.remoteAddress || "unknown";
18+
const now = Date.now();
19+
const entry = rateLimitMap.get(ip);
20+
21+
if (!entry || now - entry.windowStart > RATE_WINDOW) {
22+
rateLimitMap.set(ip, { count: 1, windowStart: now });
23+
return next();
24+
}
25+
26+
entry.count++;
27+
if (entry.count > RATE_LIMIT) {
28+
const retryAfter = Math.ceil((RATE_WINDOW - (now - entry.windowStart)) / 1000);
29+
res.set("Retry-After", retryAfter);
30+
return res.status(429).json({
31+
error: `Too many requests. Try again in ${retryAfter}s.`,
32+
});
33+
}
34+
35+
next();
36+
}
37+
38+
setInterval(() => {
39+
const now = Date.now();
40+
for (const [ip, entry] of rateLimitMap.entries()) {
41+
if (now - entry.windowStart > RATE_WINDOW) rateLimitMap.delete(ip);
42+
}
43+
}, RATE_WINDOW);
44+
45+
router.post("/extract", rateLimit, upload.single("bin"), async (req, res) => {
1346
try {
1447
if (!req.file) return res.status(400).json({ error: "No file uploaded" });
1548

@@ -29,7 +62,7 @@ router.post("/extract", upload.single("bin"), async (req, res) => {
2962
}
3063
});
3164

32-
router.post("/frame/:index", upload.single("bin"), async (req, res) => {
65+
router.post("/frame/:index", rateLimit, upload.single("bin"), async (req, res) => {
3366
try {
3467
if (!req.file) return res.status(400).json({ error: "No file uploaded" });
3568

@@ -50,16 +83,15 @@ router.post("/frame/:index", upload.single("bin"), async (req, res) => {
5083

5184
router.post(
5285
"/replace",
86+
rateLimit,
5387
upload.fields([{ name: "bin" }, { name: "image" }]),
5488
async (req, res) => {
5589
try {
5690
const binFile = req.files?.["bin"]?.[0];
5791
const imageFile = req.files?.["image"]?.[0];
5892

5993
if (!binFile || !imageFile) {
60-
return res
61-
.status(400)
62-
.json({ error: "Both bin and image files are required" });
94+
return res.status(400).json({ error: "Both bin and image files are required" });
6395
}
6496

6597
const frameIndex = parseInt(req.body.frameIndex, 10);
@@ -79,6 +111,15 @@ router.post(
79111

80112
const rowSize = Math.ceil((width * 3) / 4) * 4;
81113
const pixelDataSize = rowSize * height;
114+
const minSize = 54 + pixelDataSize;
115+
116+
if (decompressedSize < minSize) {
117+
return res.status(400).json({
118+
error: `Frame ${frameIndex} decompressedSize (${decompressedSize}) is smaller than ` +
119+
`minimum BMP size (${minSize}) for ${width}×${height}. File may be corrupt.`,
120+
});
121+
}
122+
82123
const bmpBuffer = Buffer.alloc(decompressedSize);
83124

84125
bmpBuffer[0] = 0x42;

src/services/logoBin.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ const { parseBmpHeader } = require("../utils/bmp");
55
const inflate = promisify(zlib.gunzip);
66
const deflate = promisify(zlib.gzip);
77

8-
const HEADER_SIZE = 44;
98
const MAGIC = Buffer.from([0x47, 0x5a]);
9+
const SIZES_OFFSET = 24;
10+
const SIZE_ENTRY = 4;
11+
12+
function computeHeaderSize(frameCount) {
13+
return SIZES_OFFSET + frameCount * SIZE_ENTRY;
14+
}
1015

1116
function validateMagic(buffer) {
1217
return buffer[0] === MAGIC[0] && buffer[1] === MAGIC[1];
@@ -15,17 +20,26 @@ function validateMagic(buffer) {
1520
function readFrameSizes(buffer, count) {
1621
const sizes = [];
1722
for (let i = 0; i < count; i++) {
18-
sizes.push(buffer.readUInt32LE(24 + i * 4));
23+
sizes.push(buffer.readUInt32LE(SIZES_OFFSET + i * SIZE_ENTRY));
1924
}
2025
return sizes;
2126
}
2227

2328
async function extractFrames(buffer) {
2429
if (!validateMagic(buffer)) {
25-
throw new Error("Invalid logo.bin: expected GZ magic");
30+
throw new Error("Invalid logo.bin: expected GZ magic bytes (0x47 0x5A)");
2631
}
2732

2833
const frameCount = buffer.readUInt32LE(2);
34+
if (frameCount === 0 || frameCount > 64) {
35+
throw new Error(`Unexpected frame count: ${frameCount}. File may be corrupt or unsupported.`);
36+
}
37+
38+
const HEADER_SIZE = computeHeaderSize(frameCount);
39+
if (buffer.length < HEADER_SIZE) {
40+
throw new Error("File too small to contain a valid header.");
41+
}
42+
2943
const compressedSizes = readFrameSizes(buffer, frameCount);
3044

3145
let offset = HEADER_SIZE;
@@ -54,10 +68,11 @@ async function extractFrames(buffer) {
5468

5569
async function repackFrames(originalBuffer, frameIndex, newBmpBuffer) {
5670
if (!validateMagic(originalBuffer)) {
57-
throw new Error("Invalid logo.bin: expected GZ magic");
71+
throw new Error("Invalid logo.bin: expected GZ magic bytes (0x47 0x5A)");
5872
}
5973

6074
const frameCount = originalBuffer.readUInt32LE(2);
75+
const HEADER_SIZE = computeHeaderSize(frameCount);
6176
const compressedSizes = readFrameSizes(originalBuffer, frameCount);
6277

6378
let offset = HEADER_SIZE;
@@ -79,7 +94,7 @@ async function repackFrames(originalBuffer, frameIndex, newBmpBuffer) {
7994
output.writeUInt32LE(frameCount, 2);
8095

8196
for (let i = 0; i < frameCount; i++) {
82-
output.writeUInt32LE(newSizes[i], 24 + i * 4);
97+
output.writeUInt32LE(newSizes[i], SIZES_OFFSET + i * SIZE_ENTRY);
8398
}
8499

85100
let writeOffset = HEADER_SIZE;

0 commit comments

Comments
 (0)