Compare commits

...
4 Commits
Author SHA1 Message Date
calixteman 735ea6285c Merge pull request #22036 from Prafyl/fix-rgb-srcpos
Use `srcPos` for the remaining pixels in `convertRGBToRGBA`
2026-10-01 12:27:12 +02:00
calixteman ebd61ab636 Merge pull request #22037 from calixteman/cff-index-offsets
Bound CFF INDEX and FDArray parsing
2026-10-01 12:25:27 +02:00
calixteman e130a42923 Bound CFF INDEX and FDArray parsing
Reject invalid INDEX offset sizes and clamp offsets to prevent overlapping
entries and repeated parsing of the same data.

Limit FDArray parsing to the 256 entries addressable by FDSelect's Card8
indices. Reject format 3 ranges before they expand past the glyph count.
2026-10-01 11:43:01 +02:00
Prafyl f8bf56fd9f Use srcPos for the remaining pixels in convertRGBToRGBA
The loop for the last 1-3 pixels started at `i * 4` instead of
`srcPos + i * 4`, so with a `srcPos` offset those pixels were read from
the start of the image, and for every chunk the loop also walked over
all the data before it, which made the chunked conversion in
`putBinaryImageData` quadratic in the image height.
2026-09-30 22:17:13 +05:45
4 changed files with 124 additions and 7 deletions
+23 -5
View File
@@ -35,6 +35,9 @@ import { MathClamp } from "../shared/math_clamp.js";
// Maximum subroutine call depth of type 2 charstrings. Matches OTS.
const MAX_SUBR_NESTING = 10;
// CFF FDSelect uses Card8 indices, so only 256 font DICTs are addressable.
const MAX_FD_ARRAY_COUNT = 256;
function looksLikeUnsigned16BitNegative(coord) {
return coord > 0x7fff && coord <= 0xffff;
}
@@ -325,7 +328,12 @@ class CFFParser {
let charset, encoding;
if (cff.isCIDFont) {
const fdArrayIndex = this.parseIndex(topDict.getByName("FDArray")).obj;
for (let i = 0, ii = fdArrayIndex.count; i < ii; ++i) {
let fdArrayCount = fdArrayIndex.count;
if (fdArrayCount > MAX_FD_ARRAY_COUNT) {
warn(`CFFParser.parse: too many FDArray entries (${fdArrayCount}).`);
fdArrayCount = MAX_FD_ARRAY_COUNT;
}
for (let i = 0; i < fdArrayCount; ++i) {
const dictRaw = fdArrayIndex.get(i);
const fontDict = this.createDict(
CFFTopDict,
@@ -489,16 +497,22 @@ class CFFParser {
if (count !== 0) {
const offsetSize = bytes[pos++];
// add 1 for offset to determine size of last object
if (offsetSize < 1 || offsetSize > 4) {
throw new FormatError(`Invalid CFF INDEX offset size: ${offsetSize}`);
}
// Offsets are relative to the byte before the object data.
const startPos = pos + (count + 1) * offsetSize - 1;
const bytesLength = bytes.length;
// Clamp offsets to prevent out-of-bounds or overlapping entries.
let prevOffset = startPos;
for (i = 0, ii = count + 1; i < ii; ++i) {
let offset = 0;
for (let j = 0; j < offsetSize; ++j) {
offset <<= 8;
offset += bytes[pos++];
offset = (offset << 8) | bytes[pos++];
}
offsets.push(startPos + offset);
prevOffset = MathClamp(startPos + offset, prevOffset, bytesLength);
offsets.push(prevOffset);
}
end = offsets[count];
}
@@ -1086,6 +1100,10 @@ class CFFParser {
}
const fdIndex = bytes[pos++];
const next = (bytes[pos] << 8) | bytes[pos + 1];
// Reject ranges that would expand FDSelect past the glyph count.
if (next - first > length - fdSelect.length) {
throw new FormatError("parseFDSelect: Invalid font data.");
}
for (let j = first; j < next; ++j) {
fdSelect.push(fdIndex);
}
+2 -2
View File
@@ -102,7 +102,7 @@ function convertRGBToRGBA({
dest[destPos + 3] = (s3 >>> 8) | alphaMask;
}
for (let j = i * 4, jj = srcPos + len; j < jj; j += 3) {
for (let j = srcPos + i * 4, jj = srcPos + len; j < jj; j += 3) {
dest[destPos++] =
src[j] | (src[j + 1] << 8) | (src[j + 2] << 16) | alphaMask;
}
@@ -118,7 +118,7 @@ function convertRGBToRGBA({
dest[destPos + 3] = (s3 << 8) | alphaMask;
}
for (let j = i * 4, jj = srcPos + len; j < jj; j += 3) {
for (let j = srcPos + i * 4, jj = srcPos + len; j < jj; j += 3) {
dest[destPos++] =
(src[j] << 24) | (src[j + 1] << 16) | (src[j + 2] << 8) | alphaMask;
}
+81
View File
@@ -23,6 +23,7 @@ import {
CFFTopDict,
} from "../../src/core/cff_parser.js";
import { DefaultFileReaderFactory, TEST_PDFS_PATH } from "./test_utils.js";
import { FormatError } from "../../src/shared/util.js";
import { PDFDocument } from "../../src/core/document.js";
import { Ref } from "../../src/core/primitives.js";
import { SEAC_ANALYSIS_ENABLED } from "../../src/core/fonts_utils.js";
@@ -652,6 +653,86 @@ describe("CFFParser", function () {
expect(fdSelect.format).toEqual(3);
});
it("rejects fdselect format 3 ranges exceeding the glyph count", function () {
// prettier-ignore
const bytes = new Uint8Array([0x03, // format
0x00, 0x03, // range count
0x00, 0x00, // first gid
0x00, // font dict 0 id
0xff, 0xff, // next gid (too large)
0x01, // font dict 1 id
0x00, 0x00, // next gid (decreasing)
0x02, // font dict 2 id
0xff, 0xff // sentinel (exclusive end gid)
]);
parser.bytes = bytes.slice();
expect(() => parser.parseFDSelect(0, 4)).toThrowError(
FormatError,
"parseFDSelect: Invalid font data."
);
});
it("parses an index with invalid offsets", function () {
// prettier-ignore
const bytes = new Uint8Array([0x00, 0x04, // count
0x01, // offsetSize
0x01, // offset[0]
0x03, // offset[1]
0x01, // offset[2] (decreasing)
0xc8, // offset[3] (out of bounds)
0x02, // offset[4] (decreasing)
0x0a, 0x0b, 0x0c, 0x0d]);
parser.bytes = bytes;
const { obj: index, endPos } = parser.parseIndex(0);
expect(index.count).toEqual(4);
expect(index.get(0)).toEqual(new Uint8Array([0x0a, 0x0b]));
expect(index.get(1)).toEqual(new Uint8Array([]));
expect(index.get(2)).toEqual(new Uint8Array([0x0c, 0x0d]));
expect(index.get(3)).toEqual(new Uint8Array([]));
expect(endPos).toEqual(bytes.length);
});
it("throws on an index with an invalid offset size", function () {
// prettier-ignore
parser.bytes = new Uint8Array([0x00, 0x01, // count
0x05, // offsetSize (invalid)
0x00, 0x00, 0x00, 0x00, 0x01, // offset[0]
0x00, 0x00, 0x00, 0x00, 0x02, // offset[1]
0x0a]);
expect(() => parser.parseIndex(0)).toThrowError(
FormatError,
"Invalid CFF INDEX offset size: 5"
);
});
it("ignores unreachable FDArray entries", function () {
cff.isCIDFont = true;
cff.topDict.setByName("ROS", [0, 0, 0]);
cff.topDict.setByName("FDSelect", 0);
cff.topDict.setByName("FDArray", 0);
cff.fdArray = [];
for (let i = 0; i < 300; i++) {
const fdDict = new CFFTopDict(cff.strings);
fdDict.setByName("Private", [0, 0]);
fdDict.privateDict = new CFFPrivateDict(cff.strings);
cff.fdArray.push(fdDict);
}
cff.fdSelect = new CFFFDSelect(0, Array(cff.charStrings.count).fill(0));
const fontDataWithLargeFDArray = new CFFCompiler(cff).compile();
const reparsedCff = new CFFParser(
new Stream(fontDataWithLargeFDArray),
{},
SEAC_ANALYSIS_ENABLED
).parse();
expect(reparsedCff.fdArray.length).toEqual(256);
});
// TODO fdArray
});
+18
View File
@@ -328,5 +328,23 @@ describe("image_utils", function () {
expect(dest[2]).toEqual(RED); // red
expect(dest[3]).toEqual(0); // untouched
});
it("handles srcPos offset for RGB_24BPP", function () {
// Four black pixels to skip, then one red pixel read from srcPos=12.
const src = new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0,
]);
const dest = new Uint32Array(1);
const result = convertToRGBA({
src,
dest,
srcPos: 12,
width: 1,
height: 1,
kind: ImageKind.RGB_24BPP,
});
expect(result.srcPos).toEqual(15);
expect(dest[0]).toEqual(RED);
});
});
});