Skip to content

Commit

Permalink
base64 urlSafe. Closes #1746
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Jun 2, 2019
1 parent d7ff4fa commit 295b97b
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 18 deletions.
3 changes: 2 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -2589,7 +2589,8 @@ const schema = Joi.string().hex();
Requires the string value to be a valid base64 string; does not check the decoded value.

- `options` - optional settings:
- `paddingRequired` - optional parameter defaulting to `true` which will require `=` padding if `true` or make padding optional if `false`.
- `paddingRequired` - if `true`, the string must be properly padded with the `=` characters. Defaults to `true`.
- `urlSafe` - if `true`, uses the URI-safe base64 format which replaces `+` with `-` and `\` with `_`. Defaults to `false`.

Padding characters are not required for decoding, as the number of missing bytes can be inferred from the number of digits. With that said, try to use padding if at all possible.

Expand Down
18 changes: 14 additions & 4 deletions lib/types/string/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@ const Uri = require('./uri');

const internals = {
base64Regex: {
true: /^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/,
false: /^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}(==)?|[A-Za-z0-9+\/]{3}=?)?$/
// paddingRequired
true: {
// urlSafe
true: /^(?:[\w\-]{2}[\w\-]{2})*(?:[\w\-]{2}==|[\w\-]{3}=)?$/,
false: /^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/
},
false: {
true: /^(?:[\w\-]{2}[\w\-]{2})*(?:[\w\-]{2}(==)?|[\w\-]{3}=?)?$/,
false: /^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}(==)?|[A-Za-z0-9+\/]{3}=?)?$/
}
},
dataUriRegex: {
format: /^data:[\w+.-]+\/[\w+.-]+;((charset=[\w-]+|base64),)?(.*)$/,
base64: {
// paddingRequired
true: /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/,
false: /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}(==)?|[A-Za-z0-9+\/]{3}=?)?$/
}
Expand Down Expand Up @@ -131,8 +140,9 @@ internals.String = class extends Any {

Hoek.assert(typeof options === 'object', 'options must be an object');

options = { paddingRequired: true, ...options };
options = { urlSafe: false, paddingRequired: true, ...options };
Hoek.assert(typeof options.paddingRequired === 'boolean', 'paddingRequired must be boolean');
Hoek.assert(typeof options.urlSafe === 'boolean', 'urlSafe must be boolean');

return this._rule('base64', { args: { options } });
}
Expand Down Expand Up @@ -512,7 +522,7 @@ internals.String.prototype._rules = {

base64: function (value, helpers, { options }) {

const regex = internals.base64Regex[options.paddingRequired];
const regex = internals.base64Regex[options.paddingRequired][options.urlSafe];
if (regex.test(value)) {
return value;
}
Expand Down
105 changes: 92 additions & 13 deletions test/types/string.js
Original file line number Diff line number Diff line change
Expand Up @@ -1909,7 +1909,7 @@ describe('string', () => {
it('should include inverted pattern name if specified', () => {

const schema = Joi.string().regex(/[a-z]/, {
name : 'lowercase',
name: 'lowercase',
invert: true
});
Helper.validate(schema, [
Expand Down Expand Up @@ -1944,7 +1944,7 @@ describe('string', () => {
details: [{
message,
path: [],
type: /versions/.test(message) ? 'string.ipVersion' : 'string.ip',
type: /versions/.test(message) ? 'string.ipVersion' : 'string.ip',
context: (() => {

const context = {
Expand Down Expand Up @@ -2100,7 +2100,7 @@ describe('string', () => {
'7:6:5:4:3:2:1::'
]);

const validIPvFuturesWithCidr = prepareIps(['v1.09azAZ-._~!$&\'()*+,;=:/32','v1.09azAZ-._~!$&\'()*+,;=:/128']);
const validIPvFuturesWithCidr = prepareIps(['v1.09azAZ-._~!$&\'()*+,;=:/32', 'v1.09azAZ-._~!$&\'()*+,;=:/128']);

const validIPvFuturesWithoutCidr = prepareIps(['v1.09azAZ-._~!$&\'()*+,;=:']);

Expand Down Expand Up @@ -2583,7 +2583,7 @@ describe('string', () => {
Helper.validate(schema, [
['foo://example.com:8042/over/there?name=ferret#nose', true],
['https://example.com?abc[]=123&abc[]=456', false, null, {
message:'"value" must be a valid uri',
message: '"value" must be a valid uri',
details: [{
message: '"value" must be a valid uri',
path: [],
Expand Down Expand Up @@ -9901,22 +9901,17 @@ describe('string', () => {

it('validates the base64 options', () => {

expect(() => {

Joi.string().base64('a');
}).to.throw('options must be an object');

expect(() => {

Joi.string().base64({ paddingRequired: 'a' });
}).to.throw('paddingRequired must be boolean');
expect(() => Joi.string().base64('a')).to.throw('options must be an object');
expect(() => Joi.string().base64({ paddingRequired: 'a' })).to.throw('paddingRequired must be boolean');
expect(() => Joi.string().base64({ urlSafe: 'a' })).to.throw('urlSafe must be boolean');
});

it('validates a base64 string with no options', () => {

const rule = Joi.string().base64();
Helper.validate(rule, [
['YW55IGNhcm5hbCBwbGVhc3VyZS4=', true],
['YW55IGNh+/5hbCBwbGVhc3VyZS4=', true],
['=YW55IGNhcm5hbCBwbGVhc3VyZS4', false, null, {
message: '"value" must be a valid base64 string',
details: [{
Expand All @@ -9930,6 +9925,19 @@ describe('string', () => {
}
}]
}],
['YW55IGNhcm5hb-_wbGVhc3VyZS4=', false, null, {
message: '"value" must be a valid base64 string',
details: [{
message: '"value" must be a valid base64 string',
path: [],
type: 'string.base64',
context: {
value: 'YW55IGNhcm5hb-_wbGVhc3VyZS4=',
label: 'value',
key: undefined
}
}]
}],
['YW55IGNhcm5hbCBwbGVhc3VyZS4==', false, null, {
message: '"value" must be a valid base64 string',
details: [{
Expand Down Expand Up @@ -10257,6 +10265,77 @@ describe('string', () => {
]);
});

it('validates a url-safe base64 string with padding explicitly required', () => {

const rule = Joi.string().base64({ urlSafe: true, paddingRequired: true });
Helper.validate(rule, [
['YW55IGNhcm5hb-_wbGVhc3VyZS4=', true],
['=YW55IGNhcm5-_CBwbGVhc3VyZS4', false, null, {
message: '"value" must be a valid base64 string',
details: [{
message: '"value" must be a valid base64 string',
path: [],
type: 'string.base64',
context: {
value: '=YW55IGNhcm5-_CBwbGVhc3VyZS4',
label: 'value',
key: undefined
}
}]
}],
['YW55IGNhcm5+/CBwbGVhc3VyZS4=', false, null, {
message: '"value" must be a valid base64 string',
details: [{
message: '"value" must be a valid base64 string',
path: [],
type: 'string.base64',
context: {
value: 'YW55IGNhcm5+/CBwbGVhc3VyZS4=',
label: 'value',
key: undefined
}
}]
}]
]);
});

it('validates a url-safe base64 string with padding not required', () => {

const rule = Joi.string().base64({ urlSafe: true, paddingRequired: false });
Helper.validate(rule, [
['YW55IGNhcm5hbCBwbGVhc3VyZS4=', true],
['YW55IGNhcm-_bCBwbGVhc3VyZS4=', true],
['YW55IGNhcm5hbCBwbGVhc3VyZS4', true],
['YW55IGNhc-_hbCBwbGVhc3VyZS4', true],
['YW55IGNhcm5hbCBwbGVhc3VyZS4==', false, null, {
message: '"value" must be a valid base64 string',
details: [{
message: '"value" must be a valid base64 string',
path: [],
type: 'string.base64',
context: {
value: 'YW55IGNhcm5hbCBwbGVhc3VyZS4==',
label: 'value',
key: undefined
}
}]
}],
['YW55IGNhcm-_bCBwbGVhc3VyZS4==', false, null, {
message: '"value" must be a valid base64 string',
details: [{
message: '"value" must be a valid base64 string',
path: [],
type: 'string.base64',
context: {
value: 'YW55IGNhcm-_bCBwbGVhc3VyZS4==',
label: 'value',
key: undefined
}
}]
}]
]);
});

it('validates a dataUri string', () => {

const rule = Joi.string().dataUri();
Expand Down

0 comments on commit 295b97b

Please sign in to comment.