File size: 4,885 Bytes
1116189 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | /**
* Estimate data: URL byte lengths *without* allocating large buffers.
* - Fetch percent-decodes a base64 body before decoding it.
* - Node's Buffer.from(body, 'base64') sizes its backing allocation from the
* raw body, including ignored characters and content after padding.
* - Non-base64 data is percent-decoded and then encoded as UTF-8.
*/
const isHexDigit = (charCode) =>
(charCode >= 48 && charCode <= 57) ||
(charCode >= 65 && charCode <= 70) ||
(charCode >= 97 && charCode <= 102);
const isPercentEncodedByte = (str, i, len) =>
i + 2 < len && isHexDigit(str.charCodeAt(i + 1)) && isHexDigit(str.charCodeAt(i + 2));
const hexValue = (charCode) => (charCode <= 57 ? charCode - 48 : (charCode & 0xdf) - 55);
const isBase64Char = (charCode) =>
(charCode >= 65 && charCode <= 90) || // A-Z
(charCode >= 97 && charCode <= 122) || // a-z
(charCode >= 48 && charCode <= 57) || // 0-9
charCode === 43 || // +
charCode === 47 || // /
charCode === 45 || // - (base64url)
charCode === 95; // _ (base64url)
const isBase64Whitespace = (charCode) =>
charCode === 9 || charCode === 10 || charCode === 12 || charCode === 13 || charCode === 32;
const base64Bytes = (significant) => {
const groups = Math.floor(significant / 4);
const remainder = significant % 4;
return groups * 3 + (remainder === 2 ? 1 : remainder === 3 ? 2 : 0);
};
// Buffer.byteLength(body, 'base64') uses the raw string length as an allocation
// upper bound even when Buffer.from later ignores characters or stops at '='.
const estimateBase64BufferAllocation = (body) => {
const len = body.length;
let padding = 0;
if (len > 0 && body.charCodeAt(len - 1) === 61 /* '=' */) {
padding++;
if (len > 1 && body.charCodeAt(len - 2) === 61 /* '=' */) {
padding++;
}
}
return Math.floor(((len - padding) * 3) / 4);
};
const estimatePercentDecodedBase64Bytes = (body) => {
const len = body.length;
let significant = 0;
let padding = 0;
let invalid = false;
for (let i = 0; i < len; i++) {
let code = body.charCodeAt(i);
if (code === 37 /* '%' */ && isPercentEncodedByte(body, i, len)) {
code = hexValue(body.charCodeAt(i + 1)) * 16 + hexValue(body.charCodeAt(i + 2));
i += 2;
}
if (isBase64Whitespace(code)) {
continue;
}
if (code === 61 /* '=' */) {
padding++;
continue;
}
if (!isBase64Char(code) || padding > 0) {
invalid = true;
continue;
}
significant++;
}
// Fetch rejects malformed forgiving-base64 input. Returning the raw-size
// allocation bound keeps that invalid input from becoming a pre-check bypass.
if (
invalid ||
padding > 2 ||
(padding > 0 && (significant + padding) % 4 !== 0) ||
significant % 4 === 1
) {
return estimateBase64BufferAllocation(body);
}
return base64Bytes(significant);
};
const estimateDataURLBytes = (url, estimateBase64) => {
if (!url || typeof url !== 'string') return 0;
if (!url.startsWith('data:')) return 0;
const comma = url.indexOf(',');
if (comma < 0) return 0;
const meta = url.slice(5, comma);
const body = url.slice(comma + 1);
const isBase64 = /;base64/i.test(meta);
if (isBase64) {
return estimateBase64(body);
}
// Compute UTF-8 byte length directly from UTF-16 code units without allocating
// a byte buffer (TextEncoder.encode would defeat the DoS guard on large bodies).
// Valid %XX triplets count as one decoded byte; this matches the bytes that
// decodeURIComponent(body) would produce before Buffer re-encodes the string.
let bytes = 0;
for (let i = 0, len = body.length; i < len; i++) {
const c = body.charCodeAt(i);
if (c === 37 /* '%' */ && isPercentEncodedByte(body, i, len)) {
bytes += 1;
i += 2;
} else if (c < 0x80) {
bytes += 1;
} else if (c < 0x800) {
bytes += 2;
} else if (c >= 0xd800 && c <= 0xdbff && i + 1 < len) {
const next = body.charCodeAt(i + 1);
if (next >= 0xdc00 && next <= 0xdfff) {
bytes += 4;
i++;
} else {
bytes += 3;
}
} else {
bytes += 3;
}
}
return bytes;
};
/**
* Estimate the percent-decoded payload size used by Fetch data: URLs.
*
* @param {string} url
* @returns {number}
*/
export default function estimateDataURLDecodedBytes(url) {
// Fetch removes URL fragments before processing a data: URL.
const fragmentIndex = typeof url === 'string' ? url.indexOf('#') : -1;
return estimateDataURLBytes(
fragmentIndex === -1 ? url : url.slice(0, fragmentIndex),
estimatePercentDecodedBase64Bytes
);
}
/**
* Estimate the Buffer backing allocation used by Node's raw base64 decoder.
*
* @param {string} url
* @returns {number}
*/
export function estimateDataURLBufferAllocation(url) {
return estimateDataURLBytes(url, estimateBase64BufferAllocation);
}
|