Skip to content

Commit

Permalink
refactor(go): Extract GOPROXY and NOPROXY parsing utilities (#27671)
Browse files Browse the repository at this point in the history
  • Loading branch information
zharinov committed Mar 1, 2024
1 parent 6da8fdc commit be3b29e
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 324 deletions.
135 changes: 135 additions & 0 deletions lib/modules/datasource/go/goproxy-parser.spec.ts
@@ -0,0 +1,135 @@
import * as memCache from '../../../util/cache/memory';
import { parseGoproxy, parseNoproxy } from './goproxy-parser';

describe('modules/datasource/go/goproxy-parser', () => {
beforeEach(() => {
memCache.init();
});

describe('parseGoproxy', () => {
it('parses single url', () => {
const result = parseGoproxy('foo');
expect(result).toMatchObject([{ url: 'foo' }]);
});

it('parses multiple urls', () => {
const result = parseGoproxy('foo,bar|baz,qux');
expect(result).toMatchObject([
{ url: 'foo', fallback: ',' },
{ url: 'bar', fallback: '|' },
{ url: 'baz', fallback: ',' },
{ url: 'qux' },
]);
});

it('ignores everything starting from "direct" and "off" keywords', () => {
expect(parseGoproxy(undefined)).toBeEmpty();
expect(parseGoproxy(undefined)).toBeEmpty();
expect(parseGoproxy('')).toBeEmpty();
expect(parseGoproxy('off')).toMatchObject([
{ url: 'off', fallback: '|' },
]);
expect(parseGoproxy('direct')).toMatchObject([
{ url: 'direct', fallback: '|' },
]);
expect(parseGoproxy('foo,off|direct,qux')).toMatchObject([
{ url: 'foo', fallback: ',' },
{ url: 'off', fallback: '|' },
{ url: 'direct', fallback: ',' },
{ url: 'qux', fallback: '|' },
]);
});

it('caches results', () => {
expect(parseGoproxy('foo,bar')).toBe(parseGoproxy('foo,bar'));
});
});

describe('parseNoproxy', () => {
it('produces regex', () => {
expect(parseNoproxy(undefined)).toBeNull();
expect(parseNoproxy(null)).toBeNull();
expect(parseNoproxy('')).toBeNull();
expect(parseNoproxy('/')).toBeNull();
expect(parseNoproxy('*')?.source).toBe('^(?:[^\\/]*)(?:\\/.*)?$');
expect(parseNoproxy('?')?.source).toBe('^(?:[^\\/])(?:\\/.*)?$');
expect(parseNoproxy('foo')?.source).toBe('^(?:foo)(?:\\/.*)?$');
expect(parseNoproxy('\\f\\o\\o')?.source).toBe('^(?:foo)(?:\\/.*)?$');
expect(parseNoproxy('foo,bar')?.source).toBe('^(?:foo|bar)(?:\\/.*)?$');
expect(parseNoproxy('[abc]')?.source).toBe('^(?:[abc])(?:\\/.*)?$');
expect(parseNoproxy('[a-c]')?.source).toBe('^(?:[a-c])(?:\\/.*)?$');
expect(parseNoproxy('[\\a-\\c]')?.source).toBe('^(?:[a-c])(?:\\/.*)?$');
expect(parseNoproxy('a.b.c')?.source).toBe('^(?:a\\.b\\.c)(?:\\/.*)?$');
expect(parseNoproxy('trailing/')?.source).toBe(
'^(?:trailing)(?:\\/.*)?$',
);
});

it('matches on real package prefixes', () => {
expect(parseNoproxy('ex.co')?.test('ex.co/foo')).toBeTrue();
expect(parseNoproxy('ex.co/')?.test('ex.co/foo')).toBeTrue();
expect(parseNoproxy('ex.co/foo/bar')?.test('ex.co/foo/bar')).toBeTrue();
expect(parseNoproxy('ex.co/foo/bar')?.test('ex.co/foo/bar')).toBeTrue();
expect(parseNoproxy('*/foo/*')?.test('example.com/foo/bar')).toBeTrue();
expect(parseNoproxy('ex.co/foo/*')?.test('ex.co/foo/bar')).toBeTrue();
expect(parseNoproxy('ex.co/foo/*')?.test('ex.co/foo/baz')).toBeTrue();
expect(parseNoproxy('ex.co')?.test('ex.co/foo/v2')).toBeTrue();

expect(parseNoproxy('ex.co/foo/bar')?.test('ex.co/foo/bar')).toBeTrue();
expect(parseNoproxy('*/foo/*')?.test('example.com/foo/bar')).toBeTrue();
expect(parseNoproxy('ex.co/foo/*')?.test('ex.co/foo/bar')).toBeTrue();
expect(parseNoproxy('ex.co/foo/*')?.test('ex.co/foo/baz')).toBeTrue();
expect(
parseNoproxy('ex.co/foo/bar,ex.co/foo/baz')?.test('ex.co/foo/bar'),
).toBeTrue();
expect(
parseNoproxy('ex.co/foo/bar,ex.co/foo/baz')?.test('ex.co/foo/baz'),
).toBeTrue();
expect(
parseNoproxy('ex.co/foo/bar,ex.co/foo/baz')?.test('ex.co/foo/qux'),
).toBeFalse();

expect(parseNoproxy('ex')?.test('ex.co/foo')).toBeFalse();

expect(parseNoproxy('aba')?.test('x/aba')).toBeFalse();
expect(parseNoproxy('x/b')?.test('x/aba')).toBeFalse();
expect(parseNoproxy('x/ab')?.test('x/aba')).toBeFalse();
expect(parseNoproxy('x/ab[a-b]')?.test('x/aba')).toBeTrue();
});

it('matches on wildcards', () => {
expect(parseNoproxy('/*/')?.test('ex.co/foo')).toBeFalse();
expect(parseNoproxy('*/foo')?.test('ex.co/foo')).toBeTrue();
expect(parseNoproxy('*/fo')?.test('ex.co/foo')).toBeFalse();
expect(parseNoproxy('*/fo?')?.test('ex.co/foo')).toBeTrue();
expect(parseNoproxy('*/fo*')?.test('ex.co/foo')).toBeTrue();
expect(parseNoproxy('*fo*')?.test('ex.co/foo')).toBeFalse();

expect(parseNoproxy('*.co')?.test('ex.co/foo')).toBeTrue();
expect(parseNoproxy('ex*')?.test('ex.co/foo')).toBeTrue();
expect(parseNoproxy('*/foo')?.test('ex.co/foo/v2')).toBeTrue();
expect(parseNoproxy('*/foo/')?.test('ex.co/foo/v2')).toBeTrue();
expect(parseNoproxy('*/foo/*')?.test('ex.co/foo/v2')).toBeTrue();
expect(parseNoproxy('*/foo/*/')?.test('ex.co/foo/v2')).toBeTrue();
expect(parseNoproxy('*/v2')?.test('ex.co/foo/v2')).toBeFalse();
expect(parseNoproxy('*/*/v2')?.test('ex.co/foo/v2')).toBeTrue();
expect(parseNoproxy('*/*/*')?.test('ex.co/foo/v2')).toBeTrue();
expect(parseNoproxy('*/*/*/')?.test('ex.co/foo/v2')).toBeTrue();
expect(parseNoproxy('*/*/*')?.test('ex.co/foo')).toBeFalse();
expect(parseNoproxy('*/*/*/')?.test('ex.co/foo')).toBeFalse();

expect(parseNoproxy('*/*/*,,')?.test('ex.co/repo')).toBeFalse();
expect(parseNoproxy('*/*/*,,*/repo')?.test('ex.co/repo')).toBeTrue();
expect(parseNoproxy(',,*/repo')?.test('ex.co/repo')).toBeTrue();
});

it('matches on character ranges', () => {
expect(parseNoproxy('x/ab[a-b]')?.test('x/aba')).toBeTrue();
expect(parseNoproxy('x/ab[a-b]')?.test('x/abc')).toBeFalse();
});

it('caches results', () => {
expect(parseNoproxy('foo/bar')).toBe(parseNoproxy('foo/bar'));
});
});
});
115 changes: 115 additions & 0 deletions lib/modules/datasource/go/goproxy-parser.ts
@@ -0,0 +1,115 @@
import is from '@sindresorhus/is';
import moo from 'moo';
import * as memCache from '../../../util/cache/memory';
import { regEx } from '../../../util/regex';
import type { GoproxyItem } from './types';

/**
* Parse `GOPROXY` to the sequence of url + fallback strategy tags.
*
* @example
* parseGoproxy('foo.example.com|bar.example.com,baz.example.com')
* // [
* // { url: 'foo.example.com', fallback: '|' },
* // { url: 'bar.example.com', fallback: ',' },
* // { url: 'baz.example.com', fallback: '|' },
* // ]
*
* @see https://golang.org/ref/mod#goproxy-protocol
*/
export function parseGoproxy(
input: string | undefined = process.env.GOPROXY,
): GoproxyItem[] {
if (!is.string(input)) {
return [];
}

const cacheKey = `goproxy::${input}`;
const cachedResult = memCache.get<GoproxyItem[]>(cacheKey);
if (cachedResult) {
return cachedResult;
}

const result: GoproxyItem[] = input
.split(regEx(/([^,|]*(?:,|\|))/))
.filter(Boolean)
.map((s) => s.split(/(?=,|\|)/)) // TODO: #12872 lookahead
.map(([url, separator]) => ({
url,
fallback: separator === ',' ? ',' : '|',
}));

memCache.set(cacheKey, result);
return result;
}

// https://golang.org/pkg/path/#Match
const noproxyLexer = moo.states({
main: {
separator: {
match: /\s*?,\s*?/, // TODO #12870
value: (_: string) => '|',
},
asterisk: {
match: '*',
value: (_: string) => '[^/]*',
},
qmark: {
match: '?',
value: (_: string) => '[^/]',
},
characterRangeOpen: {
match: '[',
push: 'characterRange',
value: (_: string) => '[',
},
trailingSlash: {
match: /\/$/,
value: (_: string) => '',
},
char: {
match: /[^*?\\[\n]/,
value: (s: string) => s.replace(regEx('\\.', 'g'), '\\.'),
},
escapedChar: {
match: /\\./, // TODO #12870
value: (s: string) => s.slice(1),
},
},
characterRange: {
char: /[^\\\]\n]/, // TODO #12870
escapedChar: {
match: /\\./, // TODO #12870
value: (s: string) => s.slice(1),
},
characterRangeEnd: {
match: ']',
pop: 1,
},
},
});

export function parseNoproxy(
input: unknown = process.env.GONOPROXY ?? process.env.GOPRIVATE,
): RegExp | null {
if (!is.string(input)) {
return null;
}

const cacheKey = `noproxy::${input}`;
const cachedResult = memCache.get<RegExp | null>(cacheKey);
if (cachedResult !== undefined) {
return cachedResult;
}

const noproxyPattern = [...noproxyLexer.reset(input)]
.map(({ value }) => value)
.join('');

const result = noproxyPattern
? regEx(`^(?:${noproxyPattern})(?:/.*)?$`)
: null;

memCache.set(cacheKey, result);
return result;
}

0 comments on commit be3b29e

Please sign in to comment.