Skip to content

Commit 0ee6dbb

Browse files
authored
fix(filters): support Buffer input in base64_encode to prevent binary data corruption (#881)
* fix: support Buffer input in base64_encode filter When binary data (e.g. images, PDFs) is passed through the template context as a Node.js Buffer, the base64_encode filter would call stringify() on it first, which internally does String(value). This triggers Buffer.toString() with the default 'utf-8' encoding, which is a lossy conversion for non-UTF-8 byte sequences — invalid bytes get replaced with U+FFFD, permanently destroying the original data. The fix checks for Buffer.isBuffer() before stringify, and calls buffer.toString('base64') directly, bypassing the lossy UTF-8 intermediate step. String inputs continue through the existing path unchanged. Made-with: Cursor * fix: handle Buffer in filter layer to fix browser build Move Buffer handling from base64-impl.ts (which gets swapped for the browser impl at build time) into base64.ts (the filter layer). This avoids a type error during the browser rollup build where the browser impl only accepts string. Also guard Buffer.isBuffer() with typeof Buffer !== 'undefined' for safety in browser environments. Made-with: Cursor
1 parent 30e04ba commit 0ee6dbb

2 files changed

Lines changed: 33 additions & 2 deletions

File tree

src/filters/base64.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import { FilterImpl } from '../template'
88
import { stringify } from '../util'
99
import { base64Encode, base64Decode } from './base64-impl'
1010

11-
export function base64_encode (this: FilterImpl, value: string): string {
11+
export function base64_encode (this: FilterImpl, value: string | Buffer): string {
12+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
13+
this.context.memoryLimit.use(value.byteLength)
14+
return value.toString('base64')
15+
}
1216
const str = stringify(value)
1317
this.context.memoryLimit.use(str.length)
1418
return base64Encode(str)

test/integration/filters/base64.spec.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test } from '../../stub/render'
1+
import { test, liquid } from '../../stub/render'
22

33
describe('filters/base64', function () {
44
describe('base64_encode', function () {
@@ -65,6 +65,33 @@ describe('filters/base64', function () {
6565
})
6666
})
6767

68+
describe('base64_encode with Buffer input', function () {
69+
it('should encode a Buffer to base64 without data corruption', async () => {
70+
const buf = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0xfe])
71+
const result = await liquid.parseAndRender('{{ data | base64_encode }}', { data: buf })
72+
expect(result).toBe(buf.toString('base64'))
73+
})
74+
75+
it('should preserve bytes that are invalid UTF-8', async () => {
76+
const buf = Buffer.from([0x80, 0xff, 0xfe, 0x00, 0x01])
77+
const result = await liquid.parseAndRender('{{ data | base64_encode }}', { data: buf })
78+
const decoded = Buffer.from(result, 'base64')
79+
expect(decoded).toEqual(buf)
80+
})
81+
82+
it('should handle an empty Buffer', async () => {
83+
const buf = Buffer.alloc(0)
84+
const result = await liquid.parseAndRender('{{ data | base64_encode }}', { data: buf })
85+
expect(result).toBe('')
86+
})
87+
88+
it('should handle a Buffer containing valid UTF-8 text', async () => {
89+
const buf = Buffer.from('Hello World', 'utf8')
90+
const result = await liquid.parseAndRender('{{ data | base64_encode }}', { data: buf })
91+
expect(result).toBe(Buffer.from('Hello World').toString('base64'))
92+
})
93+
})
94+
6895
describe('base64 round-trip', function () {
6996
it('should encode and decode back to original', () => {
7097
return test('{{ "Hello, World!" | base64_encode | base64_decode }}', 'Hello, World!')

0 commit comments

Comments
 (0)