Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: hex prefix #3014

Merged
merged 3 commits into from
Jan 17, 2024
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
3 changes: 2 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -2823,8 +2823,9 @@ Requires the string value to be a valid hexadecimal string.

- `options` - optional settings:
- `byteAligned` - Boolean specifying whether you want to check that the hexadecimal string is byte aligned. If `convert` is `true`, a `0` will be added in front of the string in case it needs to be aligned. Defaults to `false`.
- `prefix` - Boolean or `optional`. When `true`, the string will be considered valid if prefixed with `0x` or `0X`. When `false`, the prefix is forbidden. When `optional`, the string will be considered valid if prefixed or not prefixed at all. Defaults to `false`.
```js
const schema = Joi.string().hex();
const schema = Joi.string().hex({ prefix: 'optional' });
```

Possible validation errors: [`string.hex`](#stringhex), [`string.hexAlign`](#stringhexalign)
Expand Down
9 changes: 9 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,15 @@ declare namespace Joi {
* @default false
*/
byteAligned?: boolean;
/**
* controls whether the prefix `0x` or `0X` is allowed (or required) on hex strings.
* When `true`, the prefix must be provided.
* When `false`, the prefix is forbidden.
* When `optional`, the prefix is allowed but not required.
*
* @default false
*/
prefix?: boolean | 'optional';
}

interface IpOptions {
Expand Down
18 changes: 14 additions & 4 deletions lib/types/string.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ const internals = {
}
},
dataUriRegex: /^data:[\w+.-]+\/[\w+.-]+;((charset=[\w-]+|base64),)?(.*)$/,
hexRegex: /^[a-f0-9]+$/i,
hexRegex: {
withPrefix: /^0x[0-9a-f]+$/i,
withOptionalPrefix: /^(?:0x)?[0-9a-f]+$/i,
withoutPrefix: /^[0-9a-f]+$/i
},
ipRegex: Ip.regex({ cidr: 'forbidden' }).regex,
isoDurationRegex: /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/,

Expand Down Expand Up @@ -368,16 +372,22 @@ module.exports = Any.extend({
hex: {
method(options = {}) {

Common.assertOptions(options, ['byteAligned']);
Common.assertOptions(options, ['byteAligned', 'prefix']);

options = { byteAligned: false, ...options };
options = { byteAligned: false, prefix: false, ...options };
Assert(typeof options.byteAligned === 'boolean', 'byteAligned must be boolean');
Assert(typeof options.prefix === 'boolean' || options.prefix === 'optional', 'prefix must be boolean or "optional"');

return this.$_addRule({ name: 'hex', args: { options } });
},
validate(value, helpers, { options }) {

if (!internals.hexRegex.test(value)) {
const re = options.prefix === 'optional' ?
internals.hexRegex.withOptionalPrefix :
options.prefix === true ?
internals.hexRegex.withPrefix :
internals.hexRegex.withoutPrefix;
if (!re.test(value)) {
return helpers.error('string.hex');
}

Expand Down
95 changes: 95 additions & 0 deletions test/types/string.js
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,58 @@ describe('string', () => {
]
});
});

it('describes a hex string', () => {

expect(Joi.string().hex().describe()).to.equal({
type: 'string',
rules: [{
name: 'hex',
args: {
options: {
byteAligned: false,
prefix: false
}
}
}]
});
expect(Joi.string().hex({ byteAligned: true }).describe()).to.equal({
type: 'string',
rules: [{
name: 'hex',
args: {
options: {
byteAligned: true,
prefix: false
}
}
}]
});
expect(Joi.string().hex({ prefix: true }).describe()).to.equal({
type: 'string',
rules: [{
name: 'hex',
args: {
options: {
byteAligned: false,
prefix: true
}
}
}]
});
expect(Joi.string().hex({ prefix: 'optional' }).describe()).to.equal({
type: 'string',
rules: [{
name: 'hex',
args: {
options: {
byteAligned: false,
prefix: 'optional'
}
}
}]
});
});
});

describe('domain()', () => {
Expand Down Expand Up @@ -4487,6 +4539,49 @@ describe('string', () => {
}]
]);
});

it('validates an hexadecimal string with prefix explicitly required', () => {

const rule = Joi.string().hex({ prefix: true }).strict();
Helper.validate(rule, [
['0123456789abcdef', false, {
message: '"value" must only contain hexadecimal characters',
path: [],
type: 'string.hex',
context: { value: '0123456789abcdef', label: 'value' }
}],
['0x0123456789abcdef', true],
['0X0123456789abcdef', true]
]);
});

it('validates an hexadecimal string with optional prefix', () => {

const rule = Joi.string().hex({ prefix: 'optional' }).strict();
Helper.validate(rule, [
['0123456789abcdef', true],
['0x0123456789abcdef', true],
['0X0123456789abcdef', true],
['0123456789abcdefg', false, {
message: '"value" must only contain hexadecimal characters',
path: [],
type: 'string.hex',
context: { value: '0123456789abcdefg', label: 'value' }
}],
['0x0123456789abcdefg', false, {
message: '"value" must only contain hexadecimal characters',
path: [],
type: 'string.hex',
context: { value: '0x0123456789abcdefg', label: 'value' }
}],
['0X0123456789abcdefg', false, {
message: '"value" must only contain hexadecimal characters',
path: [],
type: 'string.hex',
context: { value: '0X0123456789abcdefg', label: 'value' }
}]
]);
});
});

describe('hostname()', () => {
Expand Down