Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
3febb5d
test(base64): require Blob size preflight before read
seonghobae Aug 10, 2026
516af71
test(base64): exercise jsdom Blob read boundary
seonghobae Aug 10, 2026
14652dd
fix(base64): preflight Blob size before materializing bytes
seonghobae Aug 10, 2026
2a00a15
test(reliability): expose oversized base64 decode allocation
seonghobae Aug 11, 2026
2bc8f38
fix(reliability): preflight canonical base64 decoded size
seonghobae Aug 11, 2026
e5f84f8
test(reliability): cover canonical base64 size preflight
seonghobae Aug 11, 2026
20eceaa
test(reliability): expose percent decode allocation boundary
seonghobae Aug 11, 2026
bc4e14f
test(reliability): isolate percent decode allocation RED
seonghobae Aug 12, 2026
817061d
fix(reliability): preflight percent decode allocation
seonghobae Aug 12, 2026
5de317a
test(converter): prove runtime maxBytes fails closed
seonghobae Aug 12, 2026
a389a4f
fix(converter): validate runtime maxBytes limits
seonghobae Aug 12, 2026
f715194
test(converter): prove option containers fail closed
seonghobae Aug 12, 2026
dc160fe
fix(converter): validate exact runtime option shapes
seonghobae Aug 12, 2026
2d68023
test(converter): expose unbounded MIME override metadata
seonghobae Aug 13, 2026
61855b9
fix(converter): bound MIME override metadata
seonghobae Aug 13, 2026
0ae8e06
test(converter): expose coercible non-binary inputs
seonghobae Aug 13, 2026
757eb66
fix(converter): reject coercible non-binary inputs
seonghobae Aug 13, 2026
38b18fc
test(converter): reject Blob-like runtime impostors before property a…
seonghobae Aug 13, 2026
a94399d
fix(converter): validate Blob runtime brand before property access
seonghobae Aug 13, 2026
ff2ab2e
test(converter): preserve Blob fallbacks with real runtime values
seonghobae Aug 13, 2026
601675f
test(converter): cover real Blob arrayBuffer capability
seonghobae Aug 13, 2026
130c3c1
test(converter): prove Blob MIME accessor authority boundary
seonghobae Aug 14, 2026
75b5ca1
fix(converter): trust platform Blob MIME metadata
seonghobae Aug 14, 2026
5815630
test(converter): reject hostile data URI runtime inputs
seonghobae Aug 14, 2026
d05b4ba
fix(converter): validate data URI runtime input
seonghobae Aug 14, 2026
3de7b4f
test(converter): reject hostile data URI predicate inputs
seonghobae Aug 14, 2026
408fc5c
fix(converter): validate data URI predicate runtime input
seonghobae Aug 14, 2026
67d5410
test(converter): reject non-string base64 before member access
seonghobae Aug 14, 2026
b290a04
fix(converter): validate base64 primitive runtime input
seonghobae Aug 14, 2026
560300b
test(converter): reject malformed forgiving base64 deterministically
seonghobae Aug 14, 2026
9a1846e
fix(converter): validate forgiving base64 deterministically
seonghobae Aug 14, 2026
c06cb72
fix(converter): export malformed base64 error
seonghobae Aug 14, 2026
4129765
test(converter): reject prototype-spoofed typed arrays
seonghobae Aug 15, 2026
1d13b47
fix(converter): reject typed-array prototype spoofing
seonghobae Aug 15, 2026
afe39a5
test(converter): guard base64 byte runtime input
seonghobae Aug 15, 2026
551641c
fix(converter): validate base64 byte runtime input
seonghobae Aug 15, 2026
e16e455
test(converter): reject byte-view member interposition
seonghobae Aug 15, 2026
b850a44
fix(converter): read data-uri byte length from platform slots
seonghobae Aug 15, 2026
40caf27
test(converter): expose DataView accessor interposition
seonghobae Aug 16, 2026
73e2393
test(converter): typecheck hostile DataView regression
seonghobae Aug 16, 2026
45f89a9
test(converter): cover hostile typed-array accessors
seonghobae Aug 16, 2026
ab98ce8
fix(converter): read binary-view slots intrinsically
seonghobae Aug 16, 2026
29d2cce
test(converter): expose MIME sniff member interposition
seonghobae Aug 16, 2026
5c1ac3d
fix(converter): harden MIME sniff byte boundary
seonghobae Aug 16, 2026
8eabd33
test(converter): reject detached Uint8Array inputs
seonghobae Aug 16, 2026
6a50122
fix(converter): reject detached Uint8Array inputs
seonghobae Aug 16, 2026
e79b118
test(converter): normalize detached direct encoder input
seonghobae Aug 16, 2026
957bd0d
fix(converter): normalize detached byte views at slot boundary
seonghobae Aug 16, 2026
fe98770
test(converter): normalize detached binary views
seonghobae Aug 16, 2026
038edff
fix(converter): normalize detached ArrayBuffer views
seonghobae Aug 16, 2026
c117ded
test(converter): reject Blob byte-reader interposition
seonghobae Aug 16, 2026
f771ffe
test(converter): model platform Blob reader fallback
seonghobae Aug 16, 2026
dbc7b96
fix(converter): use intrinsic Blob byte reader
seonghobae Aug 16, 2026
d6c08be
test(converter): cover intrinsic Blob byte reader
seonghobae Aug 16, 2026
4c371c3
chore(converter): synchronize protected main
seonghobae Aug 16, 2026
56320c1
test(converter): preflight forgiving-base64 whitespace
seonghobae Aug 16, 2026
b962a38
test(converter): preserve parse precedence during base64 preflight
seonghobae Aug 16, 2026
da48f8c
fix(converter): preflight forgiving-base64 decoded size
seonghobae Aug 16, 2026
64913c2
chore(converter): synchronize with protected main
seonghobae Aug 17, 2026
e3ac4d2
fix(converter): preserve protected release controls
seonghobae Aug 17, 2026
bc23459
test(converter): expose oversized Blob MIME metadata read
seonghobae Aug 18, 2026
617a83a
test(converter): bind Blob MIME RED to platform byte reader
seonghobae Aug 18, 2026
d98ef75
fix(converter): preflight platform Blob MIME metadata
seonghobae Aug 18, 2026
1c1387d
test(converter): preserve explicit MIME precedence
seonghobae Aug 18, 2026
f3dfa92
fix(converter): preserve explicit MIME override precedence
seonghobae Aug 18, 2026
8c37030
test(converter): preserve data URI payload whitespace
seonghobae Aug 19, 2026
5ef7e16
fix(converter): preserve data URI payload whitespace
seonghobae Aug 19, 2026
dec6727
test(converter): distinguish RFC 2397 base64 flag
seonghobae Aug 19, 2026
099c159
fix(converter): require exact RFC 2397 base64 flag
seonghobae Aug 19, 2026
5921cfa
test(converter): preserve percent-encoded octets
seonghobae Aug 19, 2026
8ceb3cf
fix(converter): decode RFC 2397 percent escapes as octets
seonghobae Aug 19, 2026
af7a785
test(converter): cover percent-octet UTF-8 boundaries
seonghobae Aug 19, 2026
5129f6d
test(converter): avoid leading-whitespace URI copies
seonghobae Aug 19, 2026
e1506fc
fix(converter): parse leading URI whitespace without copying
seonghobae Aug 19, 2026
5b4d635
test(converter): reject prototype-chain classification authority
seonghobae Aug 19, 2026
7d197ee
fix(converter): classify Uint8Array by intrinsic slots
seonghobae Aug 19, 2026
1afb16b
test(converter): reject mutated Blob byte-read authority
seonghobae Aug 19, 2026
30d2d34
fix(converter): pin Blob byte-read capability
seonghobae Aug 19, 2026
6c5f838
test(converter): make Blob authority regression environment-realistic
seonghobae Aug 19, 2026
7803940
test(converter): prove mutable Blob read authority fails closed
seonghobae Aug 19, 2026
754c5d8
fix(converter): pin Blob read capability
seonghobae Aug 19, 2026
bdb0106
test(converter): pin ArrayBuffer view authority
seonghobae Aug 19, 2026
429fa3c
fix(converter): capture ArrayBuffer view classifier
seonghobae Aug 19, 2026
07da81a
test(converter): pin MIME decoder authority
seonghobae Aug 19, 2026
30d5e4f
fix(converter): capture MIME decoder authority
seonghobae Aug 19, 2026
1058cd6
test(converter): reject live global Buffer authority
seonghobae Aug 19, 2026
ce661cb
fix(converter): capture Node Buffer codec authority
seonghobae Aug 19, 2026
45ad5d3
test(converter): reject mutable Buffer.from authority
seonghobae Aug 19, 2026
8fbcd2d
fix(converter): capture Buffer.from capability
seonghobae Aug 19, 2026
6755726
test(converter): cover Bufferless codec initialization
seonghobae Aug 19, 2026
a393fd8
test(converter): avoid mutating Vitest Buffer authority
seonghobae Aug 20, 2026
98c9d1f
test(converter): mark browser-only buffer initialization
seonghobae Aug 20, 2026
1754029
test(converter): expose Buffer startup coverage gap
seonghobae Aug 21, 2026
1b5d037
fix(converter): cover Buffer startup authority
seonghobae Aug 21, 2026
ecbf842
fix(converter): keep Buffer authority declarations portable
seonghobae Aug 21, 2026
5321c9d
test(converter): bound data URI MIME metadata
seonghobae Aug 22, 2026
42b3a50
fix(converter): bound data URI MIME metadata
seonghobae Aug 22, 2026
d998236
test(converter): specify inclusive size guidance boundary
seonghobae Aug 26, 2026
0727ecb
fix(converter): align size guidance with inclusive limit
seonghobae Aug 26, 2026
dbb9c03
fix(converter): preserve stable oversize signal in guidance
seonghobae Aug 26, 2026
ae101c8
chore: synchronize converter preflight with protected main
seonghobae Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/converter/base64.buffer-authority.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { describe, expect, it } from 'vitest';
import { base64ToBytes, bytesToBase64 } from './base64.js';

type BufferFrom = typeof globalThis.Buffer.from;

describe('base64 runtime authority', () => {
it('does not let a later global Buffer replacement become codec authority', () => {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'Buffer');
if (descriptor === undefined) {
throw new Error('global Buffer descriptor is unavailable in the Node test runtime.');
}
const privateSentinel = new Error('private global Buffer sentinel');

Object.defineProperty(globalThis, 'Buffer', {
configurable: true,
writable: true,
value: {
from(): never {
throw privateSentinel;
},
},
});

try {
expect(bytesToBase64(new Uint8Array([1, 2, 3, 4]))).toBe('AQIDBA==');
expect(Array.from(base64ToBytes('AQIDBA=='))).toEqual([1, 2, 3, 4]);
} finally {
Object.defineProperty(globalThis, 'Buffer', descriptor);
}
});

it('does not let a later Buffer.from replacement become codec authority', () => {
const buffer = globalThis.Buffer;
if (buffer === undefined) {
throw new Error('global Buffer is unavailable in the Node test runtime.');
}
const descriptor = Object.getOwnPropertyDescriptor(buffer, 'from');
if (descriptor === undefined) {
throw new Error('Buffer.from descriptor is unavailable in the Node test runtime.');
}
const privateSentinel = new Error('private Buffer.from sentinel');

Object.defineProperty(buffer, 'from', {
...descriptor,
value(): never {
throw privateSentinel;
},
});

try {
expect(bytesToBase64(new Uint8Array([1, 2, 3, 4]))).toBe('AQIDBA==');
expect(Array.from(base64ToBytes('AQIDBA=='))).toEqual([1, 2, 3, 4]);
} finally {
Object.defineProperty(buffer, 'from', descriptor);
}
});

it('covers browser startup Buffer resolution without mutating Vitest globals', async () => {
const converterModule = (await import('./base64.js')) as unknown as Record<
string,
unknown
>;
const resolver = converterModule.resolveNodeBufferFrom;

expect(resolver).toBeTypeOf('function');
if (typeof resolver !== 'function') {
throw new Error('Buffer authority resolver is unavailable.');
}

const resolveNodeBufferFrom = resolver as (
buffer: typeof globalThis.Buffer | undefined,
) => BufferFrom | undefined;
expect(resolveNodeBufferFrom(undefined)).toBeUndefined();

const buffer = globalThis.Buffer;
if (buffer === undefined) {
throw new Error('global Buffer is unavailable in the Node test runtime.');
}
const captured = resolveNodeBufferFrom(buffer);
expect(captured).toBeTypeOf('function');
if (captured === undefined) {
throw new Error('Node Buffer.from authority was not captured.');
}
expect(captured('AQIDBA==', 'base64').toString('hex')).toBe('01020304');
});
});
166 changes: 143 additions & 23 deletions src/converter/base64.fallbacks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ describe('toUint8Array with a non-Uint8Array view', () => {
expect(out).toBeInstanceOf(Uint8Array);
expect(Array.from(out)).toEqual([8, 7, 6]);
});

it('does not let a later ArrayBuffer.isView replacement become classification authority', () => {
const descriptor = Object.getOwnPropertyDescriptor(ArrayBuffer, 'isView');
if (descriptor === undefined) {
throw new Error('ArrayBuffer.isView descriptor is unavailable.');
}
const privateSentinel = new Error('private ArrayBuffer.isView sentinel');
Object.defineProperty(ArrayBuffer, 'isView', {
...descriptor,
value(): boolean {
throw privateSentinel;
},
});

try {
const src = new Uint8Array([9, 8, 7, 6, 5]);
const view = new Int8Array(src.buffer, 1, 3);
const out = toUint8Array(view);
expect(Array.from(out)).toEqual([8, 7, 6]);
} finally {
Object.defineProperty(ArrayBuffer, 'isView', descriptor);
}
});
});

// Note: the `btoa`/`atob` fallback in bytesToBase64/base64ToBytes only runs in
Expand All @@ -29,21 +52,113 @@ describe('readBlobBytes environment fallbacks', () => {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
]);

it('uses Blob.arrayBuffer when the blob implements it', async () => {
const fakeBlob = {
type: 'application/octet-stream',
arrayBuffer: () => Promise.resolve(BYTES.buffer.slice(0)),
} as unknown as Blob;
const uri = await blobToDataUri(fakeBlob);
expect(uri.startsWith('data:application/octet-stream;base64,')).toBe(true);
const withoutPlatformArrayBuffer = async (
action: () => Promise<void>,
): Promise<void> => {
const descriptor = Object.getOwnPropertyDescriptor(
Blob.prototype,
'arrayBuffer',
);
if (descriptor === undefined) {
await action();
return;
}
if (!Reflect.deleteProperty(Blob.prototype, 'arrayBuffer')) {
throw new Error('Blob.prototype.arrayBuffer is not configurable.');
}
try {
await action();
} finally {
Object.defineProperty(Blob.prototype, 'arrayBuffer', descriptor);
}
};

const withPlatformArrayBuffer = async (
action: () => Promise<void>,
): Promise<void> => {
const descriptor = Object.getOwnPropertyDescriptor(
Blob.prototype,
'arrayBuffer',
);
Object.defineProperty(Blob.prototype, 'arrayBuffer', {
configurable: true,
writable: true,
value(this: Blob): Promise<ArrayBuffer> {
void this;
return Promise.resolve(BYTES.buffer.slice(0));
},
});
try {
await action();
} finally {
if (descriptor === undefined) {
Reflect.deleteProperty(Blob.prototype, 'arrayBuffer');
} else {
Object.defineProperty(Blob.prototype, 'arrayBuffer', descriptor);
}
}
};

it('uses the platform Blob.arrayBuffer capability without consulting the instance', async () => {
await withPlatformArrayBuffer(async () => {
let instanceReads = 0;
const blob = new Blob([BYTES], { type: 'application/octet-stream' });
Object.defineProperty(blob, 'arrayBuffer', {
configurable: true,
get() {
instanceReads += 1;
throw new Error('caller Blob arrayBuffer getter executed');
},
});

const uri = await blobToDataUri(blob);
expect(uri).toBe('data:application/octet-stream;base64,AQIDBA==');
expect(instanceReads).toBe(0);
});
});

it('does not let a later Blob.prototype.arrayBuffer replacement become payload authority', async () => {
const descriptor = Object.getOwnPropertyDescriptor(
Blob.prototype,
'arrayBuffer',
);
Object.defineProperty(Blob.prototype, 'arrayBuffer', {
configurable: true,
writable: true,
value(this: Blob): Promise<ArrayBuffer> {
void this;
return Promise.resolve(BYTES.buffer.slice(0));
},
});

try {
vi.resetModules();
const { blobToDataUri: isolatedBlobToDataUri } = await import('./base64.js');
const blob = new Blob([BYTES], { type: 'application/octet-stream' });

Object.defineProperty(Blob.prototype, 'arrayBuffer', {
configurable: true,
writable: true,
value(): Promise<ArrayBuffer> {
return Promise.resolve(new Uint8Array([9, 9, 9, 9]).buffer);
},
});

const uri = await isolatedBlobToDataUri(blob);
expect(uri).toBe('data:application/octet-stream;base64,AQIDBA==');
} finally {
if (descriptor === undefined) {
Reflect.deleteProperty(Blob.prototype, 'arrayBuffer');
} else {
Object.defineProperty(Blob.prototype, 'arrayBuffer', descriptor);
}
vi.resetModules();
}
});

it('falls back to application/octet-stream for a typeless, unsniffable blob', async () => {
const fakeBlob = {
type: '',
arrayBuffer: () => Promise.resolve(new Uint8Array([0, 1, 2, 3]).buffer.slice(0)),
} as unknown as Blob;
const uri = await blobToDataUri(fakeBlob);
const blob = new Blob([new Uint8Array([0, 1, 2, 3])]);
const uri = await blobToDataUri(blob);
expect(uri.startsWith('data:application/octet-stream;base64,')).toBe(true);
});

Expand All @@ -58,9 +173,10 @@ describe('readBlobBytes environment fallbacks', () => {
}
}
vi.stubGlobal('FileReader', FailingReader);
// No `arrayBuffer` method -> the FileReader branch is taken.
const fakeBlob = { type: 'image/png' } as unknown as Blob;
await expect(blobToDataUri(fakeBlob)).rejects.toThrow('reader boom');
await withoutPlatformArrayBuffer(async () => {
const blob = new Blob([PNG], { type: 'image/png' });
await expect(blobToDataUri(blob)).rejects.toThrow('reader boom');
});
});

it('rejects with a synthesized error when FileReader has no error object', async () => {
Expand All @@ -74,10 +190,12 @@ describe('readBlobBytes environment fallbacks', () => {
}
}
vi.stubGlobal('FileReader', NullErrorReader);
const fakeBlob = { type: 'image/png' } as unknown as Blob;
await expect(blobToDataUri(fakeBlob)).rejects.toThrow(
/FileReader failed to read Blob/,
);
await withoutPlatformArrayBuffer(async () => {
const blob = new Blob([PNG], { type: 'image/png' });
await expect(blobToDataUri(blob)).rejects.toThrow(
/FileReader failed to read Blob/,
);
});
});

it('reads through Response when neither arrayBuffer nor FileReader exist', async () => {
Expand All @@ -91,8 +209,10 @@ describe('readBlobBytes environment fallbacks', () => {
}
},
);
const fakeBlob = { type: 'image/png' } as unknown as Blob;
const uri = await blobToDataUri(fakeBlob);
expect(uri.startsWith('data:image/png;base64,')).toBe(true);
await withoutPlatformArrayBuffer(async () => {
const blob = new Blob([PNG], { type: 'image/png' });
const uri = await blobToDataUri(blob);
expect(uri.startsWith('data:image/png;base64,')).toBe(true);
});
});
});
});
16 changes: 16 additions & 0 deletions src/converter/base64.preflight.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, expect, it, vi } from 'vitest';
import { Base64SizeError, blobToDataUri } from './index.js';

describe('Blob size preflight', () => {
it('rejects an oversized Blob before reading payload bytes', async () => {
const blob = new Blob([new Uint8Array(8)], {
type: 'application/octet-stream',
});
const readSpy = vi.spyOn(FileReader.prototype, 'readAsArrayBuffer');

await expect(blobToDataUri(blob, { maxBytes: 4 })).rejects.toBeInstanceOf(
Base64SizeError,
);
expect(readSpy).not.toHaveBeenCalled();
});
});
Loading
Loading