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) 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..0e7dfbde1 --- /dev/null +++ b/library/src/actions/base64/base64.test.ts @@ -0,0 +1,134 @@ +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 empty string', () => { + expectNoActionIssue(action, ['']); + }); + + test('for Base64 strings', () => { + expectNoActionIssue(action, [ + // Test vectors from https://datatracker.ietf.org/doc/html/rfc4648#section-10 + '', // '' + 'Zg==', // 'f' + 'Zm8=', // 'fo' + 'Zm9v', // 'foo' + '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==', // '🌮' + ]); + }); + }); + + 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 blank strings', () => { + expectActionIssue(action, baseIssue, [' ', '\n']); + }); + + 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, [ + 'dmFsaWJvdA', // == missing + 'dmFsaWJvdA=', // = missing + 'dmFsaWJvdA===', // = extra + 'Zm9vYmE', // = missing + 'Zm9vYmE==', // = extra + ]); + }); + }); +}); 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 971764bf3..7e0f1311e 100644 --- a/library/src/actions/index.ts +++ b/library/src/actions/index.ts @@ -1,4 +1,5 @@ export * from './await/index.ts'; +export * from './base64/index.ts'; export * from './bic/index.ts'; export * from './brand/index.ts'; export * from './bytes/index.ts'; diff --git a/library/src/regex.ts b/library/src/regex.ts index 60753a389..046c6533b 100644 --- a/library/src/regex.ts +++ b/library/src/regex.ts @@ -1,7 +1,13 @@ +/** + * [Base64](https://en.wikipedia.org/wiki/Base64) regex. + */ +export const BASE64_REGEX = + /^(?:[\da-z+/]{4})*(?:[\da-z+/]{2}==|[\da-z+/]{3}=)?$/iu; + /** * [BIC](https://en.wikipedia.org/wiki/ISO_9362) regex. */ -export const BIC_REGEX: RegExp = /^[A-Z]{6}(?!00)[A-Z\d]{2}(?:[A-Z\d]{3})?$/u; +export const BIC_REGEX: RegExp = /^[A-Z]{6}(?!00)[\dA-Z]{2}(?:[\dA-Z]{3})?$/u; /** * [Cuid2](https://github.com/paralleldrive/cuid2) regex. 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/(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`.