Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { detectContentTypeFromBase64 } from './index';

const toBase64 = (input) => Buffer.from(input).toString('base64');
const repeatBytes = (sequence, times = 100) => Array.from({ length: times }, () => sequence).flat();

describe('detectContentTypeFromBase64', () => {
describe('text detection', () => {
it('detects ASCII JSON as text', () => {
expect(detectContentTypeFromBase64(toBase64('{"a":"test"}'))).toBe('text/plain');
expect(detectContentTypeFromBase64(toBase64('{\n "a": "test"\n}'))).toBe('text/plain');
});

it('detects UTF-8 JSON with Chinese characters as text', () => {
expect(detectContentTypeFromBase64(toBase64('{"a":"测试"}'))).toBe('text/plain');
expect(detectContentTypeFromBase64(toBase64('{"code":0,"msg":"操作成功","data":{"name":"张三"}}'))).toBe('text/plain');
});

it('detects UTF-8 JSON with Cyrillic characters as text', () => {
expect(detectContentTypeFromBase64(toBase64('{"message":"Привет, мир"}'))).toBe('text/plain');
});

it('detects UTF-8 text with Japanese characters and emoji as text', () => {
expect(detectContentTypeFromBase64(toBase64('{"greeting":"こんにちは世界","mood":"😀🎉"}'))).toBe('text/plain');
});

it('detects a UTF-8 body longer than the 512-byte sample as text', () => {
const body = '{"a":"' + '测'.repeat(300) + '"}';
// 6 ASCII bytes followed by 3-byte characters, so the sample is cut inside a character
expect(Buffer.byteLength(body)).toBeGreaterThan(512);
expect((512 - 6) % 3).not.toBe(0);

expect(detectContentTypeFromBase64(toBase64(body))).toBe('text/plain');
});

it('detects the first and last code points of the restricted UTF-8 ranges as text', () => {
expect(detectContentTypeFromBase64(toBase64(repeatBytes([0xE0, 0xA0, 0x80])))).toBe('text/plain'); // U+0800
expect(detectContentTypeFromBase64(toBase64(repeatBytes([0xED, 0x9F, 0xBF])))).toBe('text/plain'); // U+D7FF
expect(detectContentTypeFromBase64(toBase64(repeatBytes([0xF0, 0x90, 0x80, 0x80])))).toBe('text/plain'); // U+10000
expect(detectContentTypeFromBase64(toBase64(repeatBytes([0xF4, 0x8F, 0xBF, 0xBF])))).toBe('text/plain'); // U+10FFFF
});
});

describe('binary detection', () => {
it('detects binary formats by magic number', () => {
expect(detectContentTypeFromBase64(toBase64([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]))).toBe('image/png');
expect(detectContentTypeFromBase64(toBase64('%PDF-1.7\n'))).toBe('application/pdf');
expect(detectContentTypeFromBase64(toBase64([0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00]))).toBe('application/gzip');
});

it('detects SVG content', () => {
expect(detectContentTypeFromBase64(toBase64('<svg xmlns="http://www.w3.org/2000/svg"></svg>'))).toBe('image/svg+xml');
expect(detectContentTypeFromBase64(toBase64('<?xml version="1.0"?>\n<svg xmlns="http://www.w3.org/2000/svg"></svg>'))).toBe('image/svg+xml');
});

it('returns null for bytes that are not valid UTF-8', () => {
const bytes = [];
for (let i = 0; i < 100; i++) {
bytes.push(0x80, 0x81, 0xFE, 0xFF, 0x00);
}
expect(detectContentTypeFromBase64(toBase64(bytes))).toBe(null);
});

it('returns null for UTF-8 lead bytes without continuation bytes', () => {
const bytes = [];
for (let i = 0; i < 256; i++) {
bytes.push(0xE6, 0x61);
}
expect(detectContentTypeFromBase64(toBase64(bytes))).toBe(null);
});

it('returns null for overlong, surrogate and out-of-range UTF-8 sequences', () => {
// Overlong 3- and 4-byte encodings
expect(detectContentTypeFromBase64(toBase64(repeatBytes([0xE0, 0x80, 0x80])))).toBe(null);
expect(detectContentTypeFromBase64(toBase64(repeatBytes([0xF0, 0x80, 0x80, 0x80])))).toBe(null);
// A UTF-16 surrogate encoded as UTF-8
expect(detectContentTypeFromBase64(toBase64(repeatBytes([0xED, 0xA0, 0x80])))).toBe(null);
// A code point above U+10FFFF
expect(detectContentTypeFromBase64(toBase64(repeatBytes([0xF4, 0x90, 0x80, 0x80])))).toBe(null);
});

it('returns null for mostly binary data that contains a few UTF-8 characters', () => {
const buffer = Buffer.concat([Buffer.from('测试'), Buffer.alloc(200)]);
expect(detectContentTypeFromBase64(buffer.toString('base64'))).toBe(null);
});
});

it('returns null for empty or missing input', () => {
expect(detectContentTypeFromBase64('')).toBe(null);
expect(detectContentTypeFromBase64(undefined)).toBe(null);
expect(detectContentTypeFromBase64(null)).toBe(null);
});
});
43 changes: 43 additions & 0 deletions packages/bruno-app/src/utils/response/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,41 @@ export const escapeHtml = (text) => {
.replace(/'/g, '&#039;');
};

/**
* Returns the byte length of the UTF-8 multi-byte character starting at `index`,
* or 0 if the bytes there are not a complete, well-formed multi-byte sequence before `end`
*/
const getUtf8SequenceLength = (buffer, index, end) => {
const byte = buffer[index];
let length;
if (byte >= 0xC2 && byte <= 0xDF) {
length = 2;
} else if (byte >= 0xE0 && byte <= 0xEF) {
length = 3;
} else if (byte >= 0xF0 && byte <= 0xF4) {
length = 4;
} else {
return 0;
}

if (index + length > end) return 0;

// Reject overlong encodings, UTF-16 surrogates and code points above U+10FFFF
const secondByte = buffer[index + 1];
if ((byte === 0xE0 && secondByte < 0xA0)
|| (byte === 0xED && secondByte > 0x9F)
|| (byte === 0xF0 && secondByte < 0x90)
|| (byte === 0xF4 && secondByte > 0x8F)) {
return 0;
}

for (let j = 1; j < length; j++) {
// Continuation bytes are 10xxxxxx
if ((buffer[index + j] & 0xC0) !== 0x80) return 0;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
return length;
};

/**
* Helper to detect if buffer contains text data
*/
Expand All @@ -85,6 +120,14 @@ const isLikelyText = (buffer) => {
|| byte === 0x0A // Line feed
|| byte === 0x0D) { // Carriage return
textChars++;
continue;
}

// Non-ASCII characters (e.g. Chinese, Cyrillic, emoji) encoded as UTF-8 are text too
const sequenceLength = getUtf8SequenceLength(buffer, i, sampleSize);
if (sequenceLength > 0) {
textChars += sequenceLength;
i += sequenceLength - 1;
}
}

Expand Down