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
21 changes: 21 additions & 0 deletions docs/source/filters/base64_decode_bytes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
title: base64_decode_bytes
---

{% since %}v10.29.0{% endsince %}

Decodes a Base64-formatted string into raw bytes without interpreting the
result as UTF-8 text. It returns a `Buffer` in Node.js and a `Uint8Array` in
browsers.

Use `evalValue()` or `evalValueSync()` to preserve the binary return value.
Rendering the result directly into a template converts it to text.

```javascript
const bytes = engine.evalValueSync(
'"iVBORw0KGgr//g==" | base64_decode_bytes'
)
```

This filter is useful for binary content such as images and PDFs. For textual
content, use [`base64_decode`](./base64_decode.html) instead.
2 changes: 1 addition & 1 deletion docs/source/filters/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ HTML/URI | `escape`, `escape_once`, `url_encode`, `url_decode`, `strip_html`, `n
Array | `slice`, `map`, `sort`, `sort_natural`, `uniq`, `where`, `where_exp`, `group_by`, `group_by_exp`, `find`, `find_exp`, `first`, `last`, `join`, `reverse`, `concat`, `compact`, `size`, `push`, `pop`, `shift`, `unshift`
Date | `date`, `date_to_xmlschema`, `date_to_rfc822`, `date_to_string`, `date_to_long_string`
Misc | `default`, `json`, `jsonify`, `inspect`, `raw`, `to_integer`
Base64 | `base64_encode`, `base64_decode`
Base64 | `base64_encode`, `base64_decode`, `base64_decode_bytes`
Crypto | `sha256`, `hmac_sha256`

[shopify/liquid]: https://github.com/Shopify/liquid
14 changes: 14 additions & 0 deletions src/build/base64-impl-browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ describe('base64-impl/browser', function () {
})
})

describe('#base64DecodeBytes()', function () {
it('should decode Base64 to raw bytes without UTF-8 corruption', function () {
const result = base64.base64DecodeBytes('iVBORw0KGgr//g==')

expect(result).toEqual(
new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0xfe])
)
})

it('should decode an empty string to an empty Uint8Array', function () {
expect(base64.base64DecodeBytes('')).toEqual(new Uint8Array())
})
})

describe('round-trip encoding/decoding', function () {
it('should encode and decode back to original', function () {
const original = 'Hello, World!'
Expand Down
4 changes: 4 additions & 0 deletions src/build/base64-impl-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ export function base64Decode (str: string): string {
Uint8Array.from(atob(str), c => c.charCodeAt(0))
)
}

export function base64DecodeBytes (str: string): Uint8Array {
return Uint8Array.from(atob(str), c => c.charCodeAt(0))
}
4 changes: 4 additions & 0 deletions src/filters/base64-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ export function base64Encode (str: string): string {
export function base64Decode (str: string): string {
return Buffer.from(str, 'base64').toString('utf8')
}

export function base64DecodeBytes (str: string): Uint8Array {
return Buffer.from(str, 'base64')
}
14 changes: 13 additions & 1 deletion src/filters/base64.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { FilterImpl } from '../template'
import { stringify } from '../util'
import { base64Encode, base64Decode } from './base64-impl'
import { base64Encode, base64Decode, base64DecodeBytes } from './base64-impl'

export function base64_encode (this: FilterImpl, value: string | Buffer): string {
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
Expand All @@ -23,3 +23,15 @@ export function base64_decode (this: FilterImpl, value: string): string {
this.context.memoryLimit.use(str.length)
return base64Decode(str)
}

/**
* Decodes a Base64 string into raw bytes without interpreting them as UTF-8.
*
* Returns a Buffer in Node.js and a Uint8Array in browsers.
*/
export function base64_decode_bytes (this: FilterImpl, value: string): Uint8Array {
const str = stringify(value)
const bytes = base64DecodeBytes(str)
this.context.memoryLimit.use(bytes.byteLength)
return bytes
}
35 changes: 35 additions & 0 deletions test/integration/filters/base64.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,41 @@ describe('filters/base64', function () {
})
})

describe('base64_decode_bytes', function () {
it('should decode Base64 to raw bytes without UTF-8 corruption', () => {
const bytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0xfe])
const result = liquid.evalValueSync('data | base64_decode_bytes', {
data: bytes.toString('base64')
})

expect(Buffer.isBuffer(result)).toBe(true)
expect(result).toEqual(bytes)
})

it('should preserve bytes that are invalid UTF-8', () => {
const bytes = Buffer.from([0x80, 0xff, 0xfe, 0x00, 0x01])
const result = liquid.evalValueSync('data | base64_decode_bytes', {
data: bytes.toString('base64')
})

expect(result).toEqual(bytes)
})

it('should decode an empty string to an empty Buffer', () => {
const result = liquid.evalValueSync('data | base64_decode_bytes', { data: '' })

expect(result).toEqual(Buffer.alloc(0))
})

it('should round-trip arbitrary bytes through decode and encode filters', () => {
const bytes = Buffer.from([0x00, 0x01, 0x80, 0xff, 0xfe, 0xfd])
return test(
`{{ "${bytes.toString('base64')}" | base64_decode_bytes | base64_encode }}`,
bytes.toString('base64')
)
})
})

describe('base64_encode with Buffer input', function () {
it('should encode a Buffer to base64 without data corruption', async () => {
const buf = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0xfe])
Expand Down
Loading