Skip to content
2 changes: 1 addition & 1 deletion packages/core/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const MAX_DEPTH = 10;
export const MAX_OBJECT_SIZE = 1000;
export const MAX_ARRAY_SIZE = 1000;

export const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
export const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB

export const MAX_FILE_COUNT = 10;

Expand Down
25 changes: 20 additions & 5 deletions packages/core/src/helpers/BinaryInput.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export class BinaryInput {
private _readyPromise;
private _source: Buffer;
private _uploading: boolean = false;
// Stores load() errors so ready() can reject immediately instead of waiting for the full timeout
private _loadError: Error | null = null;

constructor(
data: BinaryInput | Buffer | ArrayBuffer | Blob | string | Record<string, any>,
Expand All @@ -29,23 +31,32 @@ export class BinaryInput {
this._name = _name;
//this._source = data;

this.load(data, _name, mimetype, candidate);
// Capture load() failures so ready() can surface them immediately rather than hanging until timeout
this.load(data, _name, mimetype, candidate).catch((err) => {
this._loadError = err instanceof Error ? err : new Error(String(err));
});
}

public async ready() {
if (this._ready) return true;

if (!this._readyPromise) {
this._readyPromise = new Promise((resolve) => {
const maxWait = 10000;
this._readyPromise = new Promise((resolve, reject) => {
let maxWait = 10000;
const interval = setInterval(() => {
if (this._ready) {
clearInterval(interval);
resolve(true);
return resolve(true);
}
// Reject as soon as a load error is known β€” avoids waiting the full 10s timeout
if (this._loadError) {
clearInterval(interval);
return reject(this._loadError);
}
maxWait -= 100;
if (maxWait <= 0) {
clearInterval(interval);
resolve(false);
return reject(new Error('BinaryInput: timed out waiting for data to load'));
}
}, 100);
});
Expand Down Expand Up @@ -208,6 +219,10 @@ export class BinaryInput {
this._ready = true;
return;
}

// Ensure load() never exits silently β€” without this, unrecognized data leaves _ready unset
// and ready() would poll for 10s before timing out with a generic error
throw new Error('BinaryInput: unsupported or invalid data format');
}

private async getUrlInfo(url) {
Expand Down
21 changes: 16 additions & 5 deletions packages/core/src/utils/base64.utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { fileTypeFromBuffer } from 'file-type';
import { isValidString } from './string.utils';
import { MAX_FILE_SIZE } from '@sre/constants';

// Inlined here to preserve utils purity (utils must not depend on code outside utils)
const MAX_BASE64_FILE_SIZE = 50 * 1024 * 1024; // 50MB

/**
* This function converts a text string to a base64 URL.
Expand Down Expand Up @@ -265,11 +267,20 @@ export function makeBase64Url(data: string, mimetype: string = 'application/octe
* @returns {string} The input string with all newline characters and escaped newline strings removed.
*/
const _cleanUpBase64Data = (str: string): string => {
// Check if the input is a string and is not excessively large
if (typeof str !== 'string' || str.length > MAX_FILE_SIZE) {
throw new Error('Invalid input');
if (typeof str !== 'string') {
throw new Error('Invalid file detected during base64 processing. Please try again.');
}

// Remove all whitespace characters and literal \n and \s sequences
return str.replace(/\s|\\n|\\s/g, '');
const cleaned = str.replace(/\s|\\n|\\s/g, '');

// Estimate the decoded binary size from the base64 string length (avoids allocating a buffer just for the check)
const estimatedBytes = Math.ceil((cleaned.length * 3) / 4);
if (estimatedBytes > MAX_BASE64_FILE_SIZE) {
const actualMB = (estimatedBytes / (1024 * 1024)).toFixed(2);
const limitMB = (MAX_BASE64_FILE_SIZE / (1024 * 1024)).toFixed(0);
throw new Error(`Invalid file detected during base64 processing: file size (~${actualMB}MB) exceeds the ${limitMB}MB limit.`);
}

return cleaned;
};
38 changes: 38 additions & 0 deletions packages/core/tests/unit/001-Core/BinaryInput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { SmythRuntime } from '@sre/Core/SmythRuntime.class';
import { describe, expect, it } from 'vitest';
import fs from 'fs';
import { IAccessCandidate, TAccessRole } from '@sre/types/ACL.types';
import { getBase64FileInfo } from '@sre/utils';
import { MAX_FILE_SIZE } from '@sre/constants';
import { setupSRE } from '../../utils/sre';
import { testData } from '../../utils/test-data-manager';

Expand Down Expand Up @@ -117,4 +119,40 @@ describe('BinaryInput Tests', () => {
const regex = new RegExp(`^smythfs:\\/\\/.*\\.${file.extension}$`);
expect(jsonData.url).toMatch(regex);
});

// --- Error path tests ---

it('should reject ready() when load() receives unsupported data', async () => {
// Passing a number (unsupported type) triggers the "unsupported or invalid data format" throw in load()
const binary = new BinaryInput(12345 as any, 'test.bin');
await expect(binary.ready()).rejects.toThrow('BinaryInput: unsupported or invalid data format');
});

it('should reject ready() when base64 data exceeds MAX_FILE_SIZE', async () => {
// Build a base64 data URL whose decoded size exceeds MAX_FILE_SIZE
// Each base64 char encodes 6 bits β†’ 4 chars = 3 bytes, so we need (MAX_FILE_SIZE + 1) * 4/3 chars
const oversizedLength = Math.ceil((MAX_FILE_SIZE + 1) * 4 / 3);
const oversizedBase64 = 'A'.repeat(oversizedLength);
const dataUrl = `data:application/octet-stream;base64,${oversizedBase64}`;

const binary = new BinaryInput(dataUrl, 'large.bin');
await expect(binary.ready()).rejects.toThrow(/exceeds the \d+MB limit/);
});
});

describe('Base64 Utils - _cleanUpBase64Data (via getBase64FileInfo)', () => {
it('should reject non-string input via getBase64FileInfo', async () => {
// _cleanUpBase64Data is private, but called internally by getBase64FileInfo
// Passing a non-string triggers the type guard; getBase64FileInfo returns null for non-base64
const result = await getBase64FileInfo(12345 as any);
expect(result).toBeNull();
});

it('should throw when base64 data exceeds MAX_FILE_SIZE', async () => {
const oversizedLength = Math.ceil((MAX_FILE_SIZE + 1) * 4 / 3);
const oversizedBase64 = 'A'.repeat(oversizedLength);
const dataUrl = `data:application/octet-stream;base64,${oversizedBase64}`;

await expect(getBase64FileInfo(dataUrl)).rejects.toThrow(/exceeds the \d+MB limit/);
});
});