Skip to content
Merged
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
33 changes: 33 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ Buffer.isEncoding = function isEncoding (encoding) {
case 'latin1':
case 'binary':
case 'base64':
case 'base64url':
case 'ucs2':
case 'ucs-2':
case 'utf16le':
Expand Down Expand Up @@ -485,6 +486,8 @@ function byteLength (string, encoding) {
return len >>> 1
case 'base64':
return base64ToBytes(string).length
case 'base64url':
return base64UrlToBytes(string).length
default:
if (loweredCase) {
return mustMatch ? -1 : utf8ToBytes(string).length // assume utf8
Expand Down Expand Up @@ -552,6 +555,9 @@ function slowToString (encoding, start, end) {
case 'base64':
return base64Slice(this, start, end)

case 'base64url':
return base64UrlSlice(this, start, end)

case 'ucs2':
case 'ucs-2':
case 'utf16le':
Expand Down Expand Up @@ -880,6 +886,10 @@ function base64Write (buf, string, offset, length) {
return blitBuffer(base64ToBytes(string), buf, offset, length)
}

function base64UrlWrite (buf, string, offset, length) {
return blitBuffer(base64UrlToBytes(string), buf, offset, length)
}

function ucs2Write (buf, string, offset, length) {
return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length)
}
Expand Down Expand Up @@ -939,6 +949,9 @@ Buffer.prototype.write = function write (string, offset, length, encoding) {
// Warning: maxLength not taken into account in base64Write
return base64Write(this, string, offset, length)

case 'base64url':
return base64UrlWrite(this, string, offset, length)

case 'ucs2':
case 'ucs-2':
case 'utf16le':
Expand Down Expand Up @@ -968,6 +981,14 @@ function base64Slice (buf, start, end) {
}
}

function base64UrlSlice (buf, start, end) {
if (start === 0 && end === buf.length) {
return base64ToBase64Url(base64.fromByteArray(buf))
} else {
return base64ToBase64Url(base64.fromByteArray(buf.slice(start, end)))
}
}

function utf8Slice (buf, start, end) {
end = Math.min(buf.length, end)
const res = []
Expand Down Expand Up @@ -2080,6 +2101,18 @@ function base64ToBytes (str) {
return base64.toByteArray(base64clean(str))
}

function base64UrlToBytes (str) {
return base64.toByteArray(base64clean(base64UrlToBase64(str)))
}

function base64ToBase64Url (str) {
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}

function base64UrlToBase64 (str) {
return str.replace(/-/g, '+').replace(/_/g, '/').padEnd(str.length + (4 - str.length % 4) % 4, '=')
}

function blitBuffer (src, dst, offset, length) {
let i
for (i = 0; i < length; ++i) {
Expand Down
92 changes: 92 additions & 0 deletions test/base64-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
const B = require('../').Buffer
const test = require('tape')

test('base64url: ignore whitespace', function (t) {
const text = '\n YW9ldQ '
const buf = new B(text, 'base64url')
t.equal(buf.toString(), 'aoeu')
t.end()
})

test('base64url: newline in utf8 -- should not be an issue', function (t) {
t.equal(
new B('LS0tCnRpdGxlOiBUaHJlZSBkYXNoZXMgbWFya3MgdGhlIHNwb3QKdGFnczoK', 'base64url').toString('utf8'),
'---\ntitle: Three dashes marks the spot\ntags:\n'
)
t.end()
})

test('base64url: newline in base64url -- should get stripped', function (t) {
t.equal(
new B('LS0tCnRpdGxlOiBUaHJlZSBkYXNoZXMgbWFya3MgdGhlIHNwb3QKdGFnczoK\nICAtIHlhbWwKICAtIGZyb250LW1hdHRlcgogIC0gZGFzaGVzCmV4cGFuZWQt', 'base64url').toString('utf8'),
'---\ntitle: Three dashes marks the spot\ntags:\n - yaml\n - front-matter\n - dashes\nexpaned-'
)
t.end()
})

test('base64url: tab characters in base64url - should get stripped', function (t) {
t.equal(
new B('LS0tCnRpdGxlOiBUaHJlZSBkYXNoZXMgbWFya3MgdGhlIHNwb3QKdGFnczoK\t\t\t\tICAtIHlhbWwKICAtIGZyb250LW1hdHRlcgogIC0gZGFzaGVzCmV4cGFuZWQt', 'base64url').toString('utf8'),
'---\ntitle: Three dashes marks the spot\ntags:\n - yaml\n - front-matter\n - dashes\nexpaned-'
)
t.end()
})

test('base64url: invalid non-alphanumeric characters -- should be stripped', function (t) {
t.equal(
new B('!"#$%&\'()*,.:;<=>?@[\\]^`{|}~', 'base64url').toString('utf8'),
''
)
t.end()
})

test('base64url: high byte', function (t) {
const highByte = B.from([128])
t.deepEqual(
B.alloc(1, highByte.toString('base64url'), 'base64url'),
highByte
)
t.end()
})

test('base64url: convert base64Url to base64', function (t) {
const base64UrlString = 'SGVsbG8td29y_bG-'
const expectedBase64 = 'SGVsbG8td29y/bG+'
const buf = new B(base64UrlString, 'base64url')
const actualBase64 = buf.toString('base64')
t.equal(actualBase64, expectedBase64)
const bufFromBase64 = new B(expectedBase64, 'base64')
t.deepEqual(buf, bufFromBase64)
const expectedBytes = [72, 101, 108, 108, 111, 45, 119, 111, 114, 253, 177, 190]
t.deepEqual(Array.from(buf), expectedBytes)
t.end()
})

test('base64url: empty and single character', function (t) {
const empty = ''
t.equal(new B(empty, 'base64url').toString('base64'), '')
const single = 'A'
const singleBuf = new B(single, 'base64url')
t.equal(singleBuf.length, 0)
t.end()
})

test('base64url: JWT-like payload', function (t) {
const jwtPayload = 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ'
const buf = new B(jwtPayload, 'base64url')
const decoded = buf.toString('utf8')
t.ok(decoded.includes('John Doe'))
t.ok(decoded.includes('1234567890'))
const base64Version = buf.toString('base64')
t.ok(base64Version.endsWith('='))
t.notEqual(base64Version, jwtPayload)
t.end()
})

test('base64url: it should accept base64 and base64url while constructing buffer with base64url', function (t) {
const buf = new B('aGVsbG93b3JsZD0=', 'base64url')
const buf2 = new B('aGVsbG93b3JsZD0=', 'base64')
t.equal(buf.toString('utf8'), buf2.toString('utf8'))
t.equal(buf.toString('utf8'), 'helloworld=')
t.end()
})