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: add base64 string validation #644

Merged
merged 8 commits into from
Jul 16, 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
4 changes: 4 additions & 0 deletions library/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 43 additions & 0 deletions library/src/actions/base64/base64.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<string, undefined>;
expectTypeOf(base64<string>()).toEqualTypeOf<Action>();
expectTypeOf(
base64<string, undefined>(undefined)
).toEqualTypeOf<Action>();
});

test('with string message', () => {
expectTypeOf(base64<string, 'message'>('message')).toEqualTypeOf<
Base64Action<string, 'message'>
>();
});

test('with function message', () => {
expectTypeOf(base64<string, () => string>(() => 'message')).toEqualTypeOf<
Base64Action<string, () => string>
>();
});
});

describe('should infer correct types', () => {
type Action = Base64Action<string, undefined>;

test('of input', () => {
expectTypeOf<InferInput<Action>>().toEqualTypeOf<string>();
});

test('of output', () => {
expectTypeOf<InferOutput<Action>>().toEqualTypeOf<string>();
});

test('of issue', () => {
expectTypeOf<InferIssue<Action>>().toEqualTypeOf<Base64Issue<string>>();
});
});
});
134 changes: 134 additions & 0 deletions library/src/actions/base64/base64.test.ts
Original file line number Diff line number Diff line change
@@ -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<Base64Action<string, never>, '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<string, undefined> = {
...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<string, string>);
});

test('with function message', () => {
const message = () => 'message';
expect(base64(message)).toStrictEqual({
...baseAction,
message,
} satisfies Base64Action<string, typeof message>);
});
});

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<Base64Issue<string>, '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
]);
});
});
});
105 changes: 105 additions & 0 deletions library/src/actions/base64/base64.ts
Original file line number Diff line number Diff line change
@@ -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<TInput extends string> extends BaseIssue<TInput> {
/**
* 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<Base64Issue<TInput>> | undefined,
> extends BaseValidation<TInput, TInput, Base64Issue<TInput>> {
/**
* 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<TInput extends string>(): 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<Base64Issue<TInput>> | undefined,
>(message: TMessage): Base64Action<TInput, TMessage>;

export function base64(
message?: ErrorMessage<Base64Issue<string>>
): Base64Action<string, ErrorMessage<Base64Issue<string>> | 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<string, Base64Issue<string>>;
},
};
}
1 change: 1 addition & 0 deletions library/src/actions/base64/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './base64.ts';
1 change: 1 addition & 0 deletions library/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
8 changes: 7 additions & 1 deletion library/src/regex.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
10 changes: 10 additions & 0 deletions website/src/routes/api/(actions)/base64/index.mdx
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 3 additions & 2 deletions website/src/routes/api/(async)/customAsync/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ The following APIs can be combined with `customAsync`.

<ApiList
items={[
'bic',
'base64',
'brand',
'bytes',
'check',
Expand Down Expand Up @@ -182,7 +182,8 @@ The following APIs can be combined with `customAsync`.
'url',
'uuid',
'value',
]}

]}
/>

### Utils
Expand Down
1 change: 1 addition & 0 deletions website/src/routes/api/(methods)/config/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ The following APIs can be combined with `config`.

<ApiList
items={[
'base64',
'bic',
'brand',
'bytes',
Expand Down
1 change: 1 addition & 0 deletions website/src/routes/api/(methods)/fallback/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ The following APIs can be combined with `fallback`.

<ApiList
items={[
'base64',
'bic',
'brand',
'bytes',
Expand Down
1 change: 1 addition & 0 deletions website/src/routes/api/(methods)/pipe/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ The following APIs can be combined with `pipe`.

<ApiList
items={[
'base64',
'bic',
'brand',
'bytes',
Expand Down
1 change: 1 addition & 0 deletions website/src/routes/api/(schemas)/any/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ The following APIs can be combined with `any`.

<ApiList
items={[
'base64',
'bic',
'brand',
'bytes',
Expand Down
1 change: 1 addition & 0 deletions website/src/routes/api/(schemas)/custom/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ The following APIs can be combined with `custom`.

<ApiList
items={[
'base64',
'bic',
'brand',
'bytes',
Expand Down
1 change: 1 addition & 0 deletions website/src/routes/api/(schemas)/intersect/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ The following APIs can be combined with `intersect`.

<ApiList
items={[
'base64',
'bic',
'brand',
'bytes',
Expand Down
Loading