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

chore: add regexp related eslint plugins #202

Merged
merged 11 commits into from
Oct 20, 2023
36 changes: 33 additions & 3 deletions library/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,45 @@ module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:regexp/recommended',
'plugin:security/recommended',
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'import'],
plugins: ['@typescript-eslint', 'import', 'redos-detector'],
rules: {
// Enable rules -----------------------------------------------------------

// Import
'import/extensions': ['error', 'always'], // Require file extensions

// Regexp
'regexp/no-super-linear-move': 'error', // Prevent DoS regexps
'regexp/no-control-character': 'error', // Avoid unneeded regexps characters
'regexp/no-octal': 'error', // Avoid unneeded regexps characters
'regexp/no-standalone-backslash': 'error', // Avoid unneeded regexps characters
'regexp/prefer-escape-replacement-dollar-char': 'error', // Avoid unneeded regexps characters
'regexp/prefer-quantifier': 'error', // Avoid unneeded regexps characters
'regexp/hexadecimal-escape': ['error', 'always'], // Avoid unneeded regexps characters
'regexp/sort-alternatives': 'error', // Avoid unneeded regexps characters
'regexp/require-unicode-regexp': 'error', // /u flag is faster and enables regexp strict mode
'regexp/prefer-regexp-exec': 'error', // Enforce that RegExp#exec is used instead of String#match if no global flag is provided, as exec is faster

// Redos detector
'redos-detector/no-unsafe-regex': ['error', { ignoreError: true }], // Prevent DoS regexps

// Disable rules ----------------------------------------------------------

// Default
'no-duplicate-imports': 'off',

// TypeScript
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/consistent-type-imports': 'warn',
'@typescript-eslint/no-non-null-assertion': 'off',
'no-duplicate-imports': 'off',
'import/extensions': ['error', 'always'],

// Security
'security/detect-object-injection': 'off', // Too many false positives
'security/detect-unsafe-regex': 'off', // Too many false positives, see https://github.com/eslint-community/eslint-plugin-security/issues/28 - we use the redos-detector plugin instead
},
};
1 change: 1 addition & 0 deletions library/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ All notable changes to the library will be documented in this file.
- Add `getRestAndDefaultArgs` utility function
- Add new `rest` argument to `object` and `objectAsync` schema
- Fix type check in `date` and `dateAsync` for invalid dates (pull request #214)
- Improve security of regular expressions (pull request #202)
- Change `ObjectSchema` and `ObjectSchemaAsync` type
- Change type check in `tuple` and `tupleAsync` to be less strict
- Rename `ObjectShape` and `ObjectShapeAsync` types to `ObjectEntries` and `ObjectEntriesAsync`
Expand Down
3 changes: 3 additions & 0 deletions library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@
"@vitest/coverage-v8": "^0.33.0",
"eslint": "^8.43.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-redos-detector": "^2.1.1",
"eslint-plugin-regexp": "^1.15.0",
"eslint-plugin-security": "^1.7.1",
"jsdom": "^22.1.0",
"tsup": "^7.1.0",
"typescript": "^5.1.3",
Expand Down
2 changes: 1 addition & 1 deletion library/src/schemas/special/special.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { special } from './special.ts';

type PixelString = `${number}px`;
const isPixelString = (input: unknown) =>
typeof input === 'string' && /^\d+px$/.test(input);
typeof input === 'string' && /^\d+px$/u.test(input);
fabian-hiller marked this conversation as resolved.
Show resolved Hide resolved

describe('special', () => {
test('should pass only pixel strings', () => {
Expand Down
2 changes: 1 addition & 1 deletion library/src/schemas/special/specialAsync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { specialAsync } from './specialAsync.ts';

type PixelString = `${number}px`;
const isPixelString = (input: unknown) =>
typeof input === 'string' && /^\d+px$/.test(input);
typeof input === 'string' && /^\d+px$/u.test(input);

describe('specialAsync', () => {
test('should pass only pixel strings', async () => {
Expand Down
2 changes: 1 addition & 1 deletion library/src/utils/isLuhnAlgo/isLuhnAlgo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
export function isLuhnAlgo(input: string) {
// Remove any non-digit chars
const number = input.replace(/\D/g, '');
const number = input.replace(/\D/gu, '');

// Create necessary variables
let length = number.length;
Expand Down
2 changes: 1 addition & 1 deletion library/src/validations/cuid2/cuid2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function cuid2<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^[a-z][a-z0-9]*$/.test(input)
!/^[a-z][\da-z]*$/u.test(input)
? getPipeIssues('cuid2', error || 'Invalid cuid2', input)
: getOutput(input);
}
6 changes: 4 additions & 2 deletions library/src/validations/email/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import type { ErrorMessage, PipeResult } from '../../types.ts';
import { getOutput, getPipeIssues } from '../../utils/index.ts';

/**
* Creates a validation function that validates an email.
* Creates a validation function that validates a email.
*
* @param error The error message.
*
* @returns A validation function.
*/
export function email<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^[\w+-]+(?:\.[\w+-]+)*@[\da-z]+(?:[.-][\da-z]+)*\.[a-z]{2,}$/i.test(input)
!/^[\w+-]+(?:\.[\w+-]+)*@[\da-z]+(?:[.-][\da-z]+)*\.[a-z]{2,}$/iu.test(
input
)
? getPipeIssues('email', error || 'Invalid email', input)
: getOutput(input);
}
2 changes: 1 addition & 1 deletion library/src/validations/emoji/emoji.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function emoji<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^(\p{Extended_Pictographic}|\p{Emoji_Component})+$/u.test(input)
!/^[\p{Extended_Pictographic}\p{Emoji_Component}]+$/u.test(input)
? getPipeIssues('emoji', error || 'Invalid emoji', input)
: getOutput(input);
}
3 changes: 1 addition & 2 deletions library/src/validations/imei/imei.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import { getOutput, getPipeIssues, isLuhnAlgo } from '../../utils/index.ts';
*/
export function imei<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^\d{2}[ |/|-]?\d{6}[ |/|-]?\d{6}[ |/|-]?\d$/.test(input) ||
!isLuhnAlgo(input)
!/^\d{2}(?:[ /|-]?\d{6}){2}[ /|-]?\d$/u.test(input) || !isLuhnAlgo(input)
? getPipeIssues('imei', error || 'Invalid IMEI', input)
: getOutput(input);
}
5 changes: 3 additions & 2 deletions library/src/validations/ip/ip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function ip<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/.test(input) &&
!/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/.test(
// eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive
!/^(?:(?:25[0-5]|(?:2[0-4]|1\d|[1-9])?\d)\.?\b){4}$/u.test(input) &&
!/^(?:(?:[\da-f]{1,4}:){7}[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,7}:|(?:[\da-f]{1,4}:){1,6}:[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,5}(?::[\da-f]{1,4}){1,2}|(?:[\da-f]{1,4}:){1,4}(?::[\da-f]{1,4}){1,3}|(?:[\da-f]{1,4}:){1,3}(?::[\da-f]{1,4}){1,4}|(?:[\da-f]{1,4}:){1,2}(?::[\da-f]{1,4}){1,5}|[\da-f]{1,4}:(?::[\da-f]{1,4}){1,6}|:(?:(?::[\da-f]{1,4}){1,7}|:)|fe80:(?::[\da-f]{0,4}){0,4}%[\da-z]+|::(?:f{4}(?::0{1,4})?:)?(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d)|(?:[\da-f]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d))$/iu.test(
input
)
? getPipeIssues('ip', error || 'Invalid IP', input)
Expand Down
3 changes: 2 additions & 1 deletion library/src/validations/ipv4/ipv4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function ipv4<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/.test(input)
// eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive
!/^(?:(?:25[0-5]|(?:2[0-4]|1\d|[1-9])?\d)\.?\b){4}$/u.test(input)
? getPipeIssues('ipv4', error || 'Invalid IP v4', input)
: getOutput(input);
}
2 changes: 1 addition & 1 deletion library/src/validations/ipv6/ipv6.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function ipv6<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/.test(
!/^(?:(?:[\da-f]{1,4}:){7}[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,7}:|(?:[\da-f]{1,4}:){1,6}:[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,5}(?::[\da-f]{1,4}){1,2}|(?:[\da-f]{1,4}:){1,4}(?::[\da-f]{1,4}){1,3}|(?:[\da-f]{1,4}:){1,3}(?::[\da-f]{1,4}){1,4}|(?:[\da-f]{1,4}:){1,2}(?::[\da-f]{1,4}){1,5}|[\da-f]{1,4}:(?::[\da-f]{1,4}){1,6}|:(?:(?::[\da-f]{1,4}){1,7}|:)|fe80:(?::[\da-f]{0,4}){0,4}%[\da-z]+|::(?:f{4}(?::0{1,4})?:)?(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d)|(?:[\da-f]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d))$/iu.test(
input
)
? getPipeIssues('ipv6', error || 'Invalid IP v6', input)
Expand Down
4 changes: 2 additions & 2 deletions library/src/validations/isoDate/isoDate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ErrorMessage, PipeResult } from '../../types.ts';
import { getOutput, getPipeIssues } from '../../utils/index.ts';

/**
* Creates a validation function that validates an date.
* Creates a validation function that validates a date.
*
* Format: yyyy-mm-dd
*
Expand All @@ -16,7 +16,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function isoDate<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])$/.test(input)
!/^\d{4}-(?:0[1-9]|1[0-2])-(?:[12]\d|0[1-9]|3[01])$/u.test(input)
? getPipeIssues('iso_date', error || 'Invalid date', input)
: getOutput(input);
}
4 changes: 2 additions & 2 deletions library/src/validations/isoDateTime/isoDateTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ErrorMessage, PipeResult } from '../../types.ts';
import { getOutput, getPipeIssues } from '../../utils/index.ts';

/**
* Creates a validation function that validates an datetime.
* Creates a validation function that validates a datetime.
*
* Format: yyyy-mm-ddThh:mm
*
Expand All @@ -16,7 +16,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function isoDateTime<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])T(0[0-9]|1\d|2[0-3]):[0-5]\d$/.test(
!/^\d{4}-(?:0[1-9]|1[0-2])-(?:[12]\d|0[1-9]|3[01])T(?:0\d|1\d|2[0-3]):[0-5]\d$/u.test(
input
)
? getPipeIssues('iso_date_time', error || 'Invalid datetime', input)
Expand Down
2 changes: 1 addition & 1 deletion library/src/validations/isoTime/isoTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function isoTime<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^(0[0-9]|1\d|2[0-3]):[0-5]\d$/.test(input)
!/^(?:0\d|1\d|2[0-3]):[0-5]\d$/u.test(input)
? getPipeIssues('iso_time', error || 'Invalid time', input)
: getOutput(input);
}
2 changes: 1 addition & 1 deletion library/src/validations/isoTimeSecond/isoTimeSecond.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function isoTimeSecond<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^(0[0-9]|1\d|2[0-3]):[0-5]\d:[0-5]\d$/.test(input)
!/^(?:0\d|1\d|2[0-3])(?::[0-5]\d){2}$/u.test(input)
? getPipeIssues('iso_time_second', error || 'Invalid time', input)
: getOutput(input);
}
2 changes: 1 addition & 1 deletion library/src/validations/isoTimestamp/isoTimestamp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function isoTimestamp<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])T(0[0-9]|1\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}Z$/.test(
!/^\d{4}-(?:0[1-9]|1[0-2])-(?:[12]\d|0[1-9]|3[01])T(?:0\d|1\d|2[0-3])(?::[0-5]\d){2}\.\d{3}Z$/u.test(
input
)
? getPipeIssues('iso_timestamp', error || 'Invalid timestamp', input)
Expand Down
2 changes: 1 addition & 1 deletion library/src/validations/isoWeek/isoWeek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function isoWeek<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^\d{4}-W(0[1-9]|[1-4]\d|5[0-3])$/.test(input)
!/^\d{4}-W(?:0[1-9]|[1-4]\d|5[0-3])$/u.test(input)
? getPipeIssues('iso_week', error || 'Invalid week', input)
: getOutput(input);
}
4 changes: 2 additions & 2 deletions library/src/validations/regex/regex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { regex } from './regex.ts';

describe('regex', () => {
test('should pass only valid strings', () => {
const validate = regex(/^ID-\d{3}$/);
const validate = regex(/^ID-\d{3}$/u);
expect(validate('ID-000').output).toBe('ID-000');
expect(validate('ID-123').output).toBe('ID-123');
expect(validate('123').issues).toBeTruthy();
Expand All @@ -13,7 +13,7 @@ describe('regex', () => {

test('should return custom error message', () => {
const error = 'Value does not match the regex!';
const validate = regex(/^ID-\d{3}$/, error);
const validate = regex(/^ID-\d{3}$/u, error);
expect(validate('test').issues?.[0].message).toBe(error);
});
});
2 changes: 1 addition & 1 deletion library/src/validations/ulid/ulid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function ulid<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^[0-9A-HJKMNPQ-TV-Z]{26}$/i.test(input)
!/^[\da-hjkmnp-tv-z]{26}$/iu.test(input)
fabian-hiller marked this conversation as resolved.
Show resolved Hide resolved
? getPipeIssues('ulid', error || 'Invalid ULID', input)
: getOutput(input);
}
4 changes: 1 addition & 3 deletions library/src/validations/uuid/uuid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function uuid<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
input
)
!/^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/iu.test(input)
? getPipeIssues('uuid', error || 'Invalid UUID', input)
: getOutput(input);
}
Loading
Loading