From c013ecaf08bc71e3addc8de3e53e78c4dc9eed68 Mon Sep 17 00:00:00 2001 From: Shinya Fujino Date: Tue, 11 Jun 2024 23:24:49 +0900 Subject: [PATCH 1/6] feat: add base64 string validation --- library/src/actions/base64/base64.test-d.ts | 43 +++++++ library/src/actions/base64/base64.test.ts | 121 ++++++++++++++++++++ library/src/actions/base64/base64.ts | 105 +++++++++++++++++ library/src/actions/base64/index.ts | 1 + library/src/actions/index.ts | 1 + library/src/regex.ts | 6 + 6 files changed, 277 insertions(+) create mode 100644 library/src/actions/base64/base64.test-d.ts create mode 100644 library/src/actions/base64/base64.test.ts create mode 100644 library/src/actions/base64/base64.ts create mode 100644 library/src/actions/base64/index.ts diff --git a/library/src/actions/base64/base64.test-d.ts b/library/src/actions/base64/base64.test-d.ts new file mode 100644 index 000000000..7c0f628e6 --- /dev/null +++ b/library/src/actions/base64/base64.test-d.ts @@ -0,0 +1,43 @@ +import { describe, expectTypeOf, test } from 'vitest'; +import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts'; +import { base64, type Base64Action, type Base64Issue } from './base64.ts'; + +describe('base64', () => { + describe('should return action object', () => { + test('with undefined message', () => { + type Action = Base64Action; + expectTypeOf(base64()).toEqualTypeOf(); + expectTypeOf( + base64(undefined) + ).toEqualTypeOf(); + }); + + test('with string message', () => { + expectTypeOf(base64('message')).toEqualTypeOf< + Base64Action + >(); + }); + + test('with function message', () => { + expectTypeOf(base64 string>(() => 'message')).toEqualTypeOf< + Base64Action string> + >(); + }); + }); + + describe('should infer correct types', () => { + type Action = Base64Action; + + test('of input', () => { + expectTypeOf>().toEqualTypeOf(); + }); + + test('of output', () => { + expectTypeOf>().toEqualTypeOf(); + }); + + test('of issue', () => { + expectTypeOf>().toEqualTypeOf>(); + }); + }); +}); diff --git a/library/src/actions/base64/base64.test.ts b/library/src/actions/base64/base64.test.ts new file mode 100644 index 000000000..cde8496c4 --- /dev/null +++ b/library/src/actions/base64/base64.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, test } from 'vitest'; +import { BASE64_REGEX } from '../../regex.ts'; +import { expectActionIssue, expectNoActionIssue } from '../../vitest/index.ts'; +import { base64, type Base64Action, type Base64Issue } from './base64.ts'; + +describe('base64', () => { + describe('should return action object', () => { + const baseAction: Omit, 'message'> = { + kind: 'validation', + type: 'base64', + reference: base64, + expects: null, + requirement: BASE64_REGEX, + async: false, + _run: expect.any(Function), + }; + + test('with undefined message', () => { + const action: Base64Action = { + ...baseAction, + message: undefined, + }; + expect(base64()).toStrictEqual(action); + expect(base64(undefined)).toStrictEqual(action); + }); + + test('with string message', () => { + expect(base64('message')).toStrictEqual({ + ...baseAction, + message: 'message', + } satisfies Base64Action); + }); + + test('with function message', () => { + const message = () => 'message'; + expect(base64(message)).toStrictEqual({ + ...baseAction, + message, + } satisfies Base64Action); + }); + }); + + describe('should return dataset without issues', () => { + const action = base64(); + + test('for untyped inputs', () => { + expect(action._run({ typed: false, value: null }, {})).toStrictEqual({ + typed: false, + value: null, + }); + }); + + test('for valid Base64 strings', () => { + expectNoActionIssue(action, [ + 'dmFsaWJvdA==', // 'valibot' + 'SGVsbG8sIEkgYW0gVmFsaWJvdCBhbmQgSSB3b3VsZCBsaWtlIHRvIGhlbHAgeW91IHZhbGlkYXRlIGRhdGEgZWFzaWx5IHVzaW5nIGEgc2NoZW1hLg==', // 'Hello, I am Valibot and I would like to help you validate data easily using a schema.' + '8J+Mrg==', // '🌮' + // Test vectors from https://datatracker.ietf.org/doc/html/rfc4648#section-10 + '', // '' + 'Zg==', // 'f' + 'Zm8=', // 'fo' + 'Zm9v', // 'foo' + 'Zm9vYg==', // 'foob' + 'Zm9vYmE=', // 'fooba' + 'Zm9vYmFy', // 'foobar' + ]); + }); + }); + + describe('should return dataset with issues', () => { + const action = base64('message'); + const baseIssue: Omit, 'input' | 'received'> = { + kind: 'validation', + type: 'base64', + expected: null, + message: 'message', + requirement: BASE64_REGEX, + }; + + test('for empty string', () => { + expectActionIssue(action, baseIssue, [' ', '\n']); + }); + + test('for invalid Base64 strings', () => { + expectActionIssue(action, baseIssue, [ + 'foo`', // invalid character '`' + 'foo~', // invalid character '~' + 'foo!', // invalid character '!' + 'foo@', // invalid character '@' + 'foo#', // invalid character '#' + 'foo$', // invalid character '$' + 'foo%', // invalid character '%' + 'foo^', // invalid character '^' + 'foo&', // invalid character '&' + 'foo*', // invalid character '*' + 'foo(', // invalid character '(' + 'foo)', // invalid character ')' + 'foo-', // invalid character '-' + 'foo_', // invalid character '_' + 'foo[', // invalid character '[' + 'foo]', // invalid character ']' + 'foo{', // invalid character '{' + 'foo}', // invalid character '}' + 'foo\\', // invalid character '\' + 'foo|', // invalid character '|' + 'foo;', // invalid character ';' + 'foo:', // invalid character ':' + "foo'", // invalid character ''' + 'foo"', // invalid character '"' + 'foo,', // invalid character ',' + 'foo.', // invalid character '.' + 'foo<', // invalid character '<' + 'foo>', // invalid character '>' + 'foo?', // invalid character '?' + 'dmFsaWJvdA', // missing padding + 'dmFsaWJvdA=', // incorrect padding + 'dmFsaWJvdA===', // incorrect padding + ]); + }); + }); +}); diff --git a/library/src/actions/base64/base64.ts b/library/src/actions/base64/base64.ts new file mode 100644 index 000000000..26f4fb1f8 --- /dev/null +++ b/library/src/actions/base64/base64.ts @@ -0,0 +1,105 @@ +import { BASE64_REGEX } from '../../regex.ts'; +import type { + BaseIssue, + BaseValidation, + Dataset, + ErrorMessage, +} from '../../types/index.ts'; +import { _addIssue } from '../../utils/index.ts'; + +/** + * Base64 issue type. + */ +export interface Base64Issue extends BaseIssue { + /** + * The issue kind. + */ + readonly kind: 'validation'; + /** + * The issue type. + */ + readonly type: 'base64'; + /** + * The expected property. + */ + readonly expected: null; + /** + * The received property. + */ + readonly received: `"${string}"`; + /** + * The Base64 regex. + */ + readonly requirement: RegExp; +} + +/** + * Base64 action type. + */ +export interface Base64Action< + TInput extends string, + TMessage extends ErrorMessage> | undefined, +> extends BaseValidation> { + /** + * The action type. + */ + readonly type: 'base64'; + /** + * The action reference. + */ + readonly reference: typeof base64; + /** + * The expected property. + */ + readonly expects: null; + /** + * The Base64 regex. + */ + readonly requirement: RegExp; + /** + * The error message. + */ + readonly message: TMessage; +} + +/** + * Creates a [Base64](https://en.wikipedia.org/wiki/Base64) validation action. + * + * @returns A Base64 action. + */ +export function base64(): Base64Action< + TInput, + undefined +>; + +/** + * Creates a [Base64](https://en.wikipedia.org/wiki/Base64) validation action. + * + * @param message The error message. + * + * @returns A Base64 action. + */ +export function base64< + TInput extends string, + const TMessage extends ErrorMessage> | undefined, +>(message: TMessage): Base64Action; + +export function base64( + message?: ErrorMessage> +): Base64Action> | undefined> { + return { + kind: 'validation', + type: 'base64', + reference: base64, + async: false, + expects: null, + requirement: BASE64_REGEX, + message, + _run(dataset, config) { + if (dataset.typed && !this.requirement.test(dataset.value)) { + _addIssue(this, 'Base64', dataset, config); + } + return dataset as Dataset>; + }, + }; +} diff --git a/library/src/actions/base64/index.ts b/library/src/actions/base64/index.ts new file mode 100644 index 000000000..51989b3c7 --- /dev/null +++ b/library/src/actions/base64/index.ts @@ -0,0 +1 @@ +export * from './base64.ts'; diff --git a/library/src/actions/index.ts b/library/src/actions/index.ts index a85792214..688a5abbf 100644 --- a/library/src/actions/index.ts +++ b/library/src/actions/index.ts @@ -1,3 +1,4 @@ +export * from './base64/index.ts'; export * from './bic/index.ts'; export * from './bytes/index.ts'; export * from './brand/index.ts'; diff --git a/library/src/regex.ts b/library/src/regex.ts index cf98c7fe2..26997a534 100644 --- a/library/src/regex.ts +++ b/library/src/regex.ts @@ -127,3 +127,9 @@ export const ULID_REGEX = /^[\da-hjkmnp-tv-z]{26}$/iu; * [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) regex. */ export const UUID_REGEX = /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/iu; + +/** + * [Base64](https://en.wikipedia.org/wiki/Base64) regex. + */ +export const BASE64_REGEX = + /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/u; From 67332163d8766d169dcf0f3c709a04751610095d Mon Sep 17 00:00:00 2001 From: Shinya Fujino Date: Tue, 11 Jun 2024 23:47:07 +0900 Subject: [PATCH 2/6] Update website --- website/src/routes/api/(actions)/base64/index.mdx | 10 ++++++++++ website/src/routes/api/(methods)/fallback/index.mdx | 1 + website/src/routes/api/(methods)/pipe/index.mdx | 1 + website/src/routes/api/(schemas)/any/index.mdx | 1 + website/src/routes/api/(schemas)/custom/index.mdx | 1 + website/src/routes/api/(schemas)/intersect/index.mdx | 1 + website/src/routes/api/(schemas)/lazy/index.mdx | 1 + website/src/routes/api/(schemas)/string/index.mdx | 1 + website/src/routes/api/(schemas)/union/index.mdx | 1 + website/src/routes/api/(schemas)/unknown/index.mdx | 1 + website/src/routes/api/menu.md | 1 + .../routes/guides/(main-concepts)/pipelines/index.mdx | 1 + 12 files changed, 21 insertions(+) create mode 100644 website/src/routes/api/(actions)/base64/index.mdx diff --git a/website/src/routes/api/(actions)/base64/index.mdx b/website/src/routes/api/(actions)/base64/index.mdx new file mode 100644 index 000000000..723f55361 --- /dev/null +++ b/website/src/routes/api/(actions)/base64/index.mdx @@ -0,0 +1,10 @@ +--- +title: base64 +source: /actions/base64/base64.ts +contributors: + - morinokami +--- + +# base64 + +> The content of this page is not yet ready. Until then, please use the [source code](https://github.com/fabian-hiller/valibot/blob/main/library/src/actions/base64/base64.ts) or take a look at [issue #287](https://github.com/fabian-hiller/valibot/issues/287) to help us extend the API reference. diff --git a/website/src/routes/api/(methods)/fallback/index.mdx b/website/src/routes/api/(methods)/fallback/index.mdx index 6d3074ba4..3525e073d 100644 --- a/website/src/routes/api/(methods)/fallback/index.mdx +++ b/website/src/routes/api/(methods)/fallback/index.mdx @@ -141,6 +141,7 @@ The following APIs can be combined with `fallback`. Date: Tue, 11 Jun 2024 23:49:38 +0900 Subject: [PATCH 3/6] Sort regex --- library/src/regex.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/library/src/regex.ts b/library/src/regex.ts index 26997a534..c98059afe 100644 --- a/library/src/regex.ts +++ b/library/src/regex.ts @@ -1,3 +1,9 @@ +/** + * [Base64](https://en.wikipedia.org/wiki/Base64) regex. + */ +export const BASE64_REGEX = + /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/u; + /** * [BIC](https://en.wikipedia.org/wiki/ISO_9362) regex. */ @@ -127,9 +133,3 @@ export const ULID_REGEX = /^[\da-hjkmnp-tv-z]{26}$/iu; * [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) regex. */ export const UUID_REGEX = /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/iu; - -/** - * [Base64](https://en.wikipedia.org/wiki/Base64) regex. - */ -export const BASE64_REGEX = - /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/u; From a327ecc43b3bdc66d8e1f987fe3711c503489cc9 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Tue, 16 Jul 2024 00:21:32 +0200 Subject: [PATCH 4/6] Improve regex and unit tests of Base64 action --- library/src/actions/base64/base64.test.ts | 89 +++++++++++++---------- library/src/regex.ts | 4 +- 2 files changed, 53 insertions(+), 40 deletions(-) diff --git a/library/src/actions/base64/base64.test.ts b/library/src/actions/base64/base64.test.ts index cde8496c4..0e7dfbde1 100644 --- a/library/src/actions/base64/base64.test.ts +++ b/library/src/actions/base64/base64.test.ts @@ -50,11 +50,12 @@ describe('base64', () => { }); }); - test('for valid Base64 strings', () => { + test('for empty string', () => { + expectNoActionIssue(action, ['']); + }); + + test('for Base64 strings', () => { expectNoActionIssue(action, [ - 'dmFsaWJvdA==', // 'valibot' - 'SGVsbG8sIEkgYW0gVmFsaWJvdCBhbmQgSSB3b3VsZCBsaWtlIHRvIGhlbHAgeW91IHZhbGlkYXRlIGRhdGEgZWFzaWx5IHVzaW5nIGEgc2NoZW1hLg==', // 'Hello, I am Valibot and I would like to help you validate data easily using a schema.' - '8J+Mrg==', // '🌮' // Test vectors from https://datatracker.ietf.org/doc/html/rfc4648#section-10 '', // '' 'Zg==', // 'f' @@ -63,6 +64,11 @@ describe('base64', () => { 'Zm9vYg==', // 'foob' 'Zm9vYmE=', // 'fooba' 'Zm9vYmFy', // 'foobar' + + // Other custom tests + 'dmFsaWJvdA==', // 'valibot' + 'SGVsbG8sIEkgYW0gVmFsaWJvdCBhbmQgSSB3b3VsZCBsaWtlIHRvIGhlbHAgeW91IHZhbGlkYXRlIGRhdGEgZWFzaWx5IHVzaW5nIGEgc2NoZW1hLg==', // 'Hello, I am Valibot and I would like to help you validate data easily using a schema.' + '8J+Mrg==', // '🌮' ]); }); }); @@ -77,44 +83,51 @@ describe('base64', () => { requirement: BASE64_REGEX, }; - test('for empty string', () => { + test('for blank strings', () => { expectActionIssue(action, baseIssue, [' ', '\n']); }); - test('for invalid Base64 strings', () => { + test('for invalid chars', () => { + expectActionIssue(action, baseIssue, [ + 'foo`', // ` + 'foo~', // ~ + 'foo!', // ! + 'foo@', // @ + 'foo#', // # + 'foo$', // $ + 'foo%', // % + 'foo^', // ^ + 'foo&', // & + 'foo*', // * + 'foo(', // ( + 'foo)', // ) + 'foo-', // - + 'foo_', // _ + 'foo[', // [ + 'foo]', // ] + 'foo{', // { + 'foo}', // } + 'foo\\', // \ + 'foo|', // | + 'foo;', // ; + 'foo:', // : + "foo'", // ' + 'foo"', // " + 'foo,', // , + 'foo.', // . + 'foo<', // < + 'foo>', // > + 'foo?', // ? + ]); + }); + + test('for invalid padding', () => { expectActionIssue(action, baseIssue, [ - 'foo`', // invalid character '`' - 'foo~', // invalid character '~' - 'foo!', // invalid character '!' - 'foo@', // invalid character '@' - 'foo#', // invalid character '#' - 'foo$', // invalid character '$' - 'foo%', // invalid character '%' - 'foo^', // invalid character '^' - 'foo&', // invalid character '&' - 'foo*', // invalid character '*' - 'foo(', // invalid character '(' - 'foo)', // invalid character ')' - 'foo-', // invalid character '-' - 'foo_', // invalid character '_' - 'foo[', // invalid character '[' - 'foo]', // invalid character ']' - 'foo{', // invalid character '{' - 'foo}', // invalid character '}' - 'foo\\', // invalid character '\' - 'foo|', // invalid character '|' - 'foo;', // invalid character ';' - 'foo:', // invalid character ':' - "foo'", // invalid character ''' - 'foo"', // invalid character '"' - 'foo,', // invalid character ',' - 'foo.', // invalid character '.' - 'foo<', // invalid character '<' - 'foo>', // invalid character '>' - 'foo?', // invalid character '?' - 'dmFsaWJvdA', // missing padding - 'dmFsaWJvdA=', // incorrect padding - 'dmFsaWJvdA===', // incorrect padding + 'dmFsaWJvdA', // == missing + 'dmFsaWJvdA=', // = missing + 'dmFsaWJvdA===', // = extra + 'Zm9vYmE', // = missing + 'Zm9vYmE==', // = extra ]); }); }); diff --git a/library/src/regex.ts b/library/src/regex.ts index c98059afe..647653529 100644 --- a/library/src/regex.ts +++ b/library/src/regex.ts @@ -2,12 +2,12 @@ * [Base64](https://en.wikipedia.org/wiki/Base64) regex. */ export const BASE64_REGEX = - /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/u; + /^(?:[\da-z+/]{4})*(?:[\da-z+/]{2}==|[\da-z+/]{3}=)?$/iu; /** * [BIC](https://en.wikipedia.org/wiki/ISO_9362) regex. */ -export const BIC_REGEX = /^[A-Z]{6}(?!00)[A-Z\d]{2}(?:[A-Z\d]{3})?$/u; +export const BIC_REGEX = /^[A-Z]{6}(?!00)[\dA-Z]{2}(?:[\dA-Z]{3})?$/u; /** * [Cuid2](https://github.com/paralleldrive/cuid2) regex. From 24ee66a6430d0e2aa48eadc7103e62aa9ad91871 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Tue, 16 Jul 2024 11:11:25 +0200 Subject: [PATCH 5/6] Update changelog of library --- library/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/CHANGELOG.md b/library/CHANGELOG.md index f2bde787a..e17766040 100644 --- a/library/CHANGELOG.md +++ b/library/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the library will be documented in this file. +## vX.X.X (Month DD, YYYY) + +- Add `base64` action to validate Base64 strings (pull request #644) + ## v0.36.0 (July 05, 2024) - Add `normalize` action to normalize strings (issue #691) From 15a96a9a3a8632daa6f9cd0ed6fdbbf7fbb12db6 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Tue, 16 Jul 2024 11:12:06 +0200 Subject: [PATCH 6/6] Add base64 action to related APIs on website --- website/src/routes/api/(async)/customAsync/index.mdx | 5 +++-- website/src/routes/api/(methods)/config/index.mdx | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/website/src/routes/api/(async)/customAsync/index.mdx b/website/src/routes/api/(async)/customAsync/index.mdx index 75b241d0d..2ca75d7ae 100644 --- a/website/src/routes/api/(async)/customAsync/index.mdx +++ b/website/src/routes/api/(async)/customAsync/index.mdx @@ -106,7 +106,7 @@ The following APIs can be combined with `customAsync`. ### Utils diff --git a/website/src/routes/api/(methods)/config/index.mdx b/website/src/routes/api/(methods)/config/index.mdx index 00701de72..329481034 100644 --- a/website/src/routes/api/(methods)/config/index.mdx +++ b/website/src/routes/api/(methods)/config/index.mdx @@ -149,6 +149,7 @@ The following APIs can be combined with `config`.