All files / src/utils api-path.ts

100% Statements 36/36
100% Branches 29/29
100% Functions 6/6
100% Lines 31/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57  276x     19506x 19506x 193236x 40510x 40510x 23x   40487x         276x 1624x 1624x 1621x       276x 593x 7x   586x       276x 263x 263x 146x 146x       276x 17300x 17296x 17279x 5x   17274x         17862x 163934x 163934x   17860x    
/** Reject path syntax that URL parsing could reinterpret before GitLab receives it. */
export function assertNoDotSegments(value: string): void {
  // WHATWG URL, URL path state: even percent-encoded dot segments shorten the path.
  // https://url.spec.whatwg.org/#path-state
  let start = 0;
  for (let end = 0; end <= value.length; end++) {
    if (end !== value.length && value[end] !== '/' && value[end] !== '\\') continue;
    const length = end - start;
    if (value[start] === '.' && (length === 1 || (length === 2 && value[start + 1] === '.'))) {
      throw new Error('Invalid API path: dot segment');
    }
    start = end + 1;
  }
}
 
/** Encode one raw API argument, preserving filenames, refs and names as one segment. */
export function encodeApiSegment(value: string | number): string {
  const text = String(value);
  assertNoDotSegments(text);
  return encodeURIComponent(text);
}
 
/** Validate a relative REST path before query serialization or any authenticated I/O. */
export function assertSafeApiPath(path: string): void {
  if (/[?#\\]/.test(path) || path.startsWith('/') || hasAsciiControlOrSpace(path)) {
    throw new Error('Invalid API path: unencoded path delimiter');
  }
  assertNoDotSegments(decodeURIComponent(path));
}
 
/** Apply the same path boundary to REST calls returning raw bytes or custom responses. */
export function assertSafeApiUrl(url: string): void {
  const start = url.indexOf('/api/v4/');
  if (start === -1) return;
  const query = url.indexOf('?', start);
  assertSafeApiPath(url.slice(start + '/api/v4/'.length, query === -1 ? undefined : query));
}
 
/** One canonical identity shared by project/group URLs and scope comparisons. */
export function canonicalProjectPath(input: string): string {
  const path = decodeURIComponent(input.trim());
  assertNoDotSegments(path);
  if (!path || /[?#\\%]/.test(path) || hasAsciiControlOrSpace(path)) {
    throw new Error('Invalid project identifier');
  }
  return path;
}
 
function hasAsciiControlOrSpace(value: string): boolean {
  // Only ASCII controls are tested; UTF-16 surrogate code units cannot match these values.
  for (let index = 0; index < value.length; index++) {
    const code = value.charCodeAt(index);
    if (code <= 32 || code === 127) return true;
  }
  return false;
}