diff options
Diffstat (limited to 'app/assets/javascripts/lib/utils/ref_validator.js')
-rw-r--r-- | app/assets/javascripts/lib/utils/ref_validator.js | 145 |
1 files changed, 145 insertions, 0 deletions
diff --git a/app/assets/javascripts/lib/utils/ref_validator.js b/app/assets/javascripts/lib/utils/ref_validator.js new file mode 100644 index 00000000000..d679a3b4198 --- /dev/null +++ b/app/assets/javascripts/lib/utils/ref_validator.js @@ -0,0 +1,145 @@ +import { __, sprintf } from '~/locale'; + +// this service validates tagName agains git ref format. +// the git spec can be found here: https://git-scm.com/docs/git-check-ref-format#_description + +// the ruby counterpart of the validator is here: +// lib/gitlab/git_ref_validator.rb + +const EXPANDED_PREFIXES = ['refs/heads/', 'refs/remotes/', 'refs/tags']; +const DISALLOWED_PREFIXES = ['-', '/']; +const DISALLOWED_POSTFIXES = ['/']; +const DISALLOWED_NAMES = ['HEAD', '@']; +const DISALLOWED_SUBSTRINGS = [' ', '\\', '~', ':', '..', '^', '?', '*', '[', '@{']; +const DISALLOWED_SEQUENCE_POSTFIXES = ['.lock', '.']; +const DISALLOWED_SEQUENCE_PREFIXES = ['.']; + +// eslint-disable-next-line no-control-regex +const CONTROL_CHARACTERS_REGEX = /[\x00-\x19\x7f]/; + +const toReadableString = (array) => array.map((item) => `"${item}"`).join(', '); + +const DisallowedPrefixesValidationMessage = sprintf( + __('Tag name should not start with %{prefixes}'), + { + prefixes: toReadableString([...EXPANDED_PREFIXES, ...DISALLOWED_PREFIXES]), + }, + false, +); + +const DisallowedPostfixesValidationMessage = sprintf( + __('Tag name should not end with %{postfixes}'), + { postfixes: toReadableString(DISALLOWED_POSTFIXES) }, + false, +); + +const DisallowedNameValidationMessage = sprintf( + __('Tag name cannot be one of the following: %{names}'), + { names: toReadableString(DISALLOWED_NAMES) }, + false, +); + +const EmptyNameValidationMessage = __('Tag name should not be empty'); + +const DisallowedSubstringsValidationMessage = sprintf( + __('Tag name should not contain any of the following: %{substrings}'), + { substrings: toReadableString(DISALLOWED_SUBSTRINGS) }, + false, +); + +const DisallowedSequenceEmptyValidationMessage = __( + `No slash-separated tag name component can be empty`, +); + +const DisallowedSequencePrefixesValidationMessage = sprintf( + __('No slash-separated component can begin with %{sequencePrefixes}'), + { sequencePrefixes: toReadableString(DISALLOWED_SEQUENCE_PREFIXES) }, + false, +); + +const DisallowedSequencePostfixesValidationMessage = sprintf( + __('No slash-separated component can end with %{sequencePostfixes}'), + { sequencePostfixes: toReadableString(DISALLOWED_SEQUENCE_POSTFIXES) }, + false, +); + +const ControlCharactersValidationMessage = __('Tag name should not contain any control characters'); + +export const validationMessages = { + EmptyNameValidationMessage, + DisallowedPrefixesValidationMessage, + DisallowedPostfixesValidationMessage, + DisallowedNameValidationMessage, + DisallowedSubstringsValidationMessage, + DisallowedSequenceEmptyValidationMessage, + DisallowedSequencePrefixesValidationMessage, + DisallowedSequencePostfixesValidationMessage, + ControlCharactersValidationMessage, +}; + +export class ValidationResult { + isValid = true; + validationErrors = []; + + addValidationError = (errorMessage) => { + this.isValid = false; + this.validationErrors.push(errorMessage); + }; +} + +export const validateTag = (refName) => { + if (typeof refName !== 'string') { + throw new Error('refName argument must be a string'); + } + + const validationResult = new ValidationResult(); + + if (!refName || refName.trim() === '') { + validationResult.addValidationError(EmptyNameValidationMessage); + return validationResult; + } + + if (CONTROL_CHARACTERS_REGEX.test(refName)) { + validationResult.addValidationError(ControlCharactersValidationMessage); + } + + if (DISALLOWED_NAMES.some((name) => name === refName)) { + validationResult.addValidationError(DisallowedNameValidationMessage); + } + + if ([...EXPANDED_PREFIXES, ...DISALLOWED_PREFIXES].some((prefix) => refName.startsWith(prefix))) { + validationResult.addValidationError(DisallowedPrefixesValidationMessage); + } + + if (DISALLOWED_POSTFIXES.some((postfix) => refName.endsWith(postfix))) { + validationResult.addValidationError(DisallowedPostfixesValidationMessage); + } + + if (DISALLOWED_SUBSTRINGS.some((substring) => refName.includes(substring))) { + validationResult.addValidationError(DisallowedSubstringsValidationMessage); + } + + const refNameParts = refName.split('/'); + + if (refNameParts.some((part) => part === '')) { + validationResult.addValidationError(DisallowedSequenceEmptyValidationMessage); + } + + if ( + refNameParts.some((part) => + DISALLOWED_SEQUENCE_PREFIXES.some((prefix) => part.startsWith(prefix)), + ) + ) { + validationResult.addValidationError(DisallowedSequencePrefixesValidationMessage); + } + + if ( + refNameParts.some((part) => + DISALLOWED_SEQUENCE_POSTFIXES.some((postfix) => part.endsWith(postfix)), + ) + ) { + validationResult.addValidationError(DisallowedSequencePostfixesValidationMessage); + } + + return validationResult; +}; |