Skip to content

Commit

Permalink
Merge pull request #644 from morinokami/feat-base64
Browse files Browse the repository at this point in the history
feat: add base64 string validation
  • Loading branch information
fabian-hiller authored Jul 16, 2024
2 parents c7addca + 15a96a9 commit 598c9a3
Show file tree
Hide file tree
Showing 21 changed files with 320 additions and 3 deletions.
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

0 comments on commit 598c9a3

Please sign in to comment.