mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-17 07:03:29 +08:00
* fix: address six verified legacy issue regressions * fix: limit issue patch to mirror URLs and CRLF imports * fix: preserve APT formats and third-party repositories
254 lines
10 KiB
JavaScript
254 lines
10 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
const fs = require('node:fs/promises');
|
|
const path = require('node:path');
|
|
const os = require('node:os');
|
|
const { spawnSync } = require('node:child_process');
|
|
const load = require('../helpers/load-security-module.cjs');
|
|
const hasApt = spawnSync('apt-get', ['--version']).status === 0;
|
|
const {
|
|
rewriteMirrorSources,
|
|
replaceMirrorUrl,
|
|
normalizeMirror,
|
|
mirrorWriteCommand,
|
|
} = require('../../back/shared/linuxMirror');
|
|
|
|
async function fixture(t) {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-issue-fixes-'));
|
|
t.after(() => fs.rm(root, { force: true, recursive: true }));
|
|
return root;
|
|
}
|
|
|
|
test('#2902/#2871 mirror writes are idempotent, preserve suites, security/ports/version paths and quote URLs', async (t) => {
|
|
const original = `# http://leave.example/debian\nTypes: deb\nURIs: http://deb.debian.org/debian\nSuites: bookworm bookworm-updates\nComponents: main\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg\n\nTypes: deb\nURIs: http://security.debian.org/debian-security\nSuites: bookworm-security\n`;
|
|
const url = 'https://mirror.example/mirrors/debian/';
|
|
const changed = rewriteMirrorSources(original, url);
|
|
assert.equal(rewriteMirrorSources(changed, url), changed);
|
|
assert.match(
|
|
changed,
|
|
/URIs: https:\/\/mirror.example\/mirrors\/debian-security/,
|
|
);
|
|
assert.match(changed, /Suites: bookworm-security/);
|
|
assert.match(changed, /# http:\/\/leave.example\/debian/);
|
|
assert.equal(
|
|
replaceMirrorUrl('http://old/debian/debian/debian', url),
|
|
'https://mirror.example/mirrors/debian',
|
|
);
|
|
assert.equal(
|
|
replaceMirrorUrl('http://old/armbian/armbian', 'https://new/armbian'),
|
|
'https://new/armbian',
|
|
);
|
|
assert.equal(
|
|
replaceMirrorUrl('http://old/ubuntu-ports', 'https://new/ubuntu'),
|
|
'https://new/ubuntu-ports',
|
|
);
|
|
assert.equal(
|
|
replaceMirrorUrl('http://old/alpine/v3.22/main', 'https://new/alpine'),
|
|
'https://new/alpine/v3.22/main',
|
|
);
|
|
assert.equal(
|
|
replaceMirrorUrl('http://old/debian', 'https://new'),
|
|
'https://new/debian',
|
|
);
|
|
for (const bad of [
|
|
'file:///tmp/a',
|
|
'http://u:p@host',
|
|
'https://host?x=1',
|
|
'https://host/\nanything',
|
|
])
|
|
assert.throws(() => normalizeMirror(bad));
|
|
const root = await fixture(t);
|
|
const file = path.join(root, "quote's", 'sources');
|
|
const command = mirrorWriteCommand(file, changed, '', 'true');
|
|
const result = spawnSync('bash', ['-c', command], { encoding: 'utf8' });
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(await fs.readFile(file, 'utf8'), changed);
|
|
});
|
|
|
|
test('#2701 shell API serializes CR/LF, quotes, backslashes and colons without corrupting JSON', async () => {
|
|
const result = spawnSync(
|
|
'bash',
|
|
[
|
|
'-c',
|
|
`source shell/api.sh
|
|
ql_api_request() { while [[ $# -gt 0 ]]; do if [[ "$1" == --data-raw ]]; then printf '%s\\n' "$2" >&3; break; fi; shift; done; printf '{"code":200}'; }
|
|
t() { :; }
|
|
add_cron_api '0 0 1 1 *' 'task a:b.js' $'quote"slash\\\\colon:\\r\\n\\t中文' 23
|
|
update_cron_api '0 0 1 1 *' 'task a:b.js' $'quote"slash\\\\colon:\\r\\n\\t中文' 27
|
|
update_cron_command_api $'task path\\\\with"quotes.js' 27
|
|
`,
|
|
],
|
|
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe', 'pipe'] },
|
|
);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
const payloads = result.output[3].trim().split('\n').map(JSON.parse);
|
|
assert.equal(payloads[0].name, 'quote"slash\\colon:\r\n\t中文');
|
|
assert.equal(payloads[0].sub_id, 23);
|
|
assert.equal(payloads[1].id, '27');
|
|
assert.equal(payloads[2].command, 'task path\\with"quotes.js');
|
|
});
|
|
|
|
for (const distribution of ['Debian', 'Ubuntu']) {
|
|
for (const legacyContent of ['', ' \t\r\n']) {
|
|
test(`${distribution}: empty legacy sources.list falls back to deb822 .sources`, async (t) => {
|
|
const root = await fixture(t);
|
|
const legacy = path.join(root, 'sources.list');
|
|
const sources = path.join(
|
|
root,
|
|
'sources.list.d',
|
|
`${distribution.toLowerCase()}.sources`,
|
|
);
|
|
await fs.writeFile(legacy, legacyContent);
|
|
const util = load('back/config/util.ts', {
|
|
'fs/promises': {
|
|
readFile: async (file) => {
|
|
if (file === '/etc/os-release')
|
|
return `ID=${distribution.toLowerCase()}\nVERSION_CODENAME=${
|
|
distribution === 'Debian' ? 'bookworm' : 'noble'
|
|
}\n`;
|
|
if (file === '/etc/apt/sources.list')
|
|
return fs.readFile(legacy, 'utf8');
|
|
throw Object.assign(new Error('missing fixture source'), {
|
|
code: 'ENOENT',
|
|
});
|
|
},
|
|
},
|
|
os: { platform: () => 'linux' },
|
|
'./index': {},
|
|
'./const': {},
|
|
'./share': {},
|
|
'./container': { isInContainer: () => false },
|
|
'../loaders/logger': {},
|
|
'../shared/utils': {},
|
|
'../shared/fileAccess': {},
|
|
'../data/dependence': {},
|
|
});
|
|
const previousOS = process.env.QL_OS_TYPE;
|
|
let command;
|
|
try {
|
|
process.env.QL_OS_TYPE = distribution.toLowerCase();
|
|
command = await util.updateLinuxMirrorFile('https://mirror.example');
|
|
} finally {
|
|
if (previousOS === undefined) delete process.env.QL_OS_TYPE;
|
|
else process.env.QL_OS_TYPE = previousOS;
|
|
}
|
|
// Execute the entire generated writer, redirecting its APT paths into
|
|
// the fixture and replacing only the package-index refresh.
|
|
const isolated = command
|
|
.replaceAll('/etc/apt', root)
|
|
.replace('apt-get update --error-on=any', 'true');
|
|
const result = spawnSync('bash', ['-c', isolated], { encoding: 'utf8' });
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(await fs.readFile(legacy, 'utf8'), legacyContent);
|
|
const generated = await fs.readFile(sources, 'utf8');
|
|
assert.match(generated, /^Types: deb\nURIs: https:\/\/mirror\.example\//);
|
|
assert.match(generated, /Signed-By: \/usr\/share\/keyrings\//);
|
|
assert.match(
|
|
generated,
|
|
distribution === 'Debian' ? /bookworm-security/ : /noble-backports/,
|
|
);
|
|
if (hasApt) {
|
|
// Parse both source locations with real APT, without refreshing indexes
|
|
// or touching the host's configuration/cache.
|
|
const parsed = spawnSync(
|
|
'apt-get',
|
|
[
|
|
'indextargets',
|
|
'-o',
|
|
`Dir::Etc::sourcelist=${legacy}`,
|
|
'-o',
|
|
`Dir::Etc::sourceparts=${path.dirname(sources)}`,
|
|
'-o',
|
|
`Dir::State::lists=${path.join(root, 'lists')}`,
|
|
],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
assert.equal(parsed.status, 0, parsed.stderr);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
test('deb822 mirror changes preserve vendor archives, signing keys and comments across repeated saves', () => {
|
|
const system =
|
|
'Types: deb\nURIs: https://deb.debian.org/debian\nSuites: bookworm\nComponents: main\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg\n';
|
|
const vendors = [
|
|
'Types: deb\nURIs: https://download.docker.com/linux/debian\nSuites: bookworm\nComponents: stable\nSigned-By: /etc/apt/keyrings/docker.asc\n',
|
|
'Types: deb\nURIs: https://download.docker.com/linux/debian\nSuites: bookworm\nComponents: stable\n',
|
|
'Types: deb\nURIs: https://packages.microsoft.com/repos/code\nSuites: stable\nComponents: main\n',
|
|
'Types: deb\nURIs: https://vendor.example/debian\nSuites: bookworm\nComponents: main\nSigned-By: /etc/apt/keyrings/vendor.gpg\n',
|
|
];
|
|
const comment = '# Keep https://vendor.example/debian unchanged\n';
|
|
const original = [system, ...vendors, comment].join('\n');
|
|
const first = rewriteMirrorSources(
|
|
original,
|
|
'https://mirror.example/mirrors/debian',
|
|
);
|
|
assert.ok(first.includes('URIs: https://mirror.example/mirrors/debian\n'));
|
|
for (const unchanged of [...vendors, comment])
|
|
assert.ok(first.includes(unchanged));
|
|
assert.equal(
|
|
rewriteMirrorSources(first, 'https://mirror.example/mirrors/debian'),
|
|
first,
|
|
);
|
|
const second = rewriteMirrorSources(first, 'https://second.example/debian');
|
|
assert.ok(second.includes('URIs: https://second.example/debian\n'));
|
|
for (const unchanged of [...vendors, comment])
|
|
assert.ok(second.includes(unchanged));
|
|
});
|
|
|
|
test('legacy mirror changes preserve third-party lines, inline comments and source options', () => {
|
|
const system =
|
|
'deb [arch=amd64 signed-by=/usr/share/keyrings/debian-archive-keyring.gpg] https://deb.debian.org/debian bookworm main # https://leave.example/debian\n';
|
|
const security =
|
|
'deb-src https://security.debian.org/debian-security bookworm-security main\n';
|
|
const vendors = [
|
|
'deb [signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable\n',
|
|
'deb https://download.docker.com/linux/debian bookworm stable\n',
|
|
'deb [signed-by=/etc/apt/keyrings/vendor.gpg] https://vendor.example/debian bookworm main\n',
|
|
'deb https://packages.microsoft.com/repos/code stable main\n',
|
|
'# deb https://deb.debian.org/debian bookworm main\n',
|
|
];
|
|
const original = system + security + vendors.join('');
|
|
const changed = rewriteMirrorSources(
|
|
original,
|
|
'https://mirror.example/debian',
|
|
);
|
|
assert.ok(
|
|
changed.includes(
|
|
system.replace(
|
|
'https://deb.debian.org/debian',
|
|
'https://mirror.example/debian',
|
|
),
|
|
),
|
|
);
|
|
assert.ok(
|
|
changed.includes(
|
|
security.replace(
|
|
'https://security.debian.org/debian-security',
|
|
'https://mirror.example/debian-security',
|
|
),
|
|
),
|
|
);
|
|
for (const unchanged of vendors) assert.ok(changed.includes(unchanged));
|
|
assert.equal(
|
|
rewriteMirrorSources(changed, 'https://mirror.example/debian'),
|
|
changed,
|
|
);
|
|
});
|
|
|
|
test('Ubuntu ports, Armbian and Alpine roots still switch mirrors without altering repository metadata', () => {
|
|
for (const url of [
|
|
'https://ports.ubuntu.com/ubuntu-ports',
|
|
'https://apt.armbian.com/armbian',
|
|
'https://old.example/alpine/v3.22/main',
|
|
'https://old.example/alpine/v3.22/community',
|
|
]) {
|
|
const original = `URIs: ${url}\nSuites: noble\nComponents: main\n`;
|
|
assert.equal(
|
|
rewriteMirrorSources(original, 'https://mirror.example'),
|
|
original.replace(url, replaceMirrorUrl(url, 'https://mirror.example')),
|
|
);
|
|
}
|
|
});
|