mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-17 07:20:51 +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
122 lines
4.1 KiB
TypeScript
122 lines
4.1 KiB
TypeScript
import { shellQuote } from './shellQuote';
|
|
|
|
const repositoryNames =
|
|
/^(debian(?:-security)?|ubuntu(?:-ports)?|alpine|armbian)$/;
|
|
|
|
export function normalizeMirror(mirror: string): URL {
|
|
if (/\s/.test(mirror)) throw new Error('Invalid Linux mirror URL');
|
|
const url = new URL(mirror);
|
|
if (
|
|
!['http:', 'https:'].includes(url.protocol) ||
|
|
url.username ||
|
|
url.password ||
|
|
url.search ||
|
|
url.hash
|
|
) {
|
|
throw new Error(
|
|
'Linux mirror must be an HTTP(S) URL without credentials, query or fragment',
|
|
);
|
|
}
|
|
return url;
|
|
}
|
|
|
|
export function replaceMirrorUrl(current: string, mirror: string): string {
|
|
const target = normalizeMirror(mirror);
|
|
const previous = new URL(current);
|
|
const oldSegments = previous.pathname.split('/').filter(Boolean);
|
|
const newSegments = target.pathname.split('/').filter(Boolean);
|
|
const repositoryIndex = oldSegments.findIndex((part) =>
|
|
repositoryNames.test(part),
|
|
);
|
|
const targetIndex = newSegments.findIndex((part) =>
|
|
repositoryNames.test(part),
|
|
);
|
|
const prefix =
|
|
targetIndex >= 0 ? newSegments.slice(0, targetIndex) : newSegments;
|
|
if (repositoryIndex >= 0) {
|
|
const repository = oldSegments[repositoryIndex];
|
|
let suffix = oldSegments.slice(repositoryIndex + 1);
|
|
// Repair paths already corrupted by repeated authority-only replacement.
|
|
while (suffix[0] === repository) suffix.shift();
|
|
return `${target.origin}/${[...prefix, repository, ...suffix].join('/')}`;
|
|
}
|
|
// Unknown repository paths: a full URL replaces the old root, a hostname
|
|
// preserves it. Never append the newly entered root to the existing root.
|
|
return `${target.origin}/${(newSegments.length
|
|
? newSegments
|
|
: oldSegments
|
|
).join('/')}`;
|
|
}
|
|
|
|
export function rewriteMirrorSources(content: string, mirror: string): string {
|
|
normalizeMirror(mirror);
|
|
return content
|
|
.split(/(\r?\n[ \t]*\r?\n)/)
|
|
.map((stanza) => {
|
|
const signedBy = stanza.match(/^Signed-By:\s*([^\r\n]*)/im)?.[1];
|
|
return stanza
|
|
.split('\n')
|
|
.map((line) => {
|
|
if (!/^\s*(?:URIs:|deb(?:-src)?\s|https?:\/\/)/.test(line))
|
|
return line;
|
|
const legacy = /^\s*deb(?:-src)?\s/.test(line);
|
|
const key = legacy
|
|
? line.match(/\bsigned-by=([^\]\s]+)/i)?.[1]
|
|
: signedBy;
|
|
// In .list files a # starts an inline comment; its URLs are metadata.
|
|
const comment = legacy ? line.indexOf('#') : -1;
|
|
const source = comment < 0 ? line : line.slice(0, comment);
|
|
const rewritten = source.replace(/https?:\/\/[^\s#]+/g, (url) =>
|
|
isSystemRepository(url, key) ? replaceMirrorUrl(url, mirror) : url,
|
|
);
|
|
return rewritten + (comment < 0 ? '' : line.slice(comment));
|
|
})
|
|
.join('\n');
|
|
})
|
|
.join('');
|
|
}
|
|
|
|
function isSystemRepository(url: string, signedBy?: string): boolean {
|
|
const keys =
|
|
signedBy
|
|
?.trim()
|
|
.split(/[\s,]+/)
|
|
.filter(Boolean) || [];
|
|
const archiveKeyring =
|
|
keys.length > 0 &&
|
|
keys.every((key) =>
|
|
/\/(?:debian|ubuntu|armbian)-archive-keyring\.(?:gpg|asc)$/.test(key),
|
|
);
|
|
// A separate signing key identifies a third-party archive, even if its
|
|
// repository happens to be called /debian or /ubuntu.
|
|
if (keys.length > 0 && !archiveKeyring) return false;
|
|
const segments = new URL(url).pathname.split('/').filter(Boolean);
|
|
const repositoryIndex = segments.findIndex((part) =>
|
|
repositoryNames.test(part),
|
|
);
|
|
if (repositoryIndex < 0) return false;
|
|
// Without archive metadata, accept only conventional archive roots. A
|
|
// vendor path such as Docker's /linux/debian is not the Debian archive.
|
|
return (
|
|
archiveKeyring ||
|
|
repositoryIndex === 0 ||
|
|
segments
|
|
.slice(0, repositoryIndex)
|
|
.every((part) => /^mirrors?$/.test(part)) ||
|
|
segments[repositoryIndex] === 'alpine'
|
|
);
|
|
}
|
|
|
|
export function mirrorWriteCommand(
|
|
file: string,
|
|
content: string,
|
|
prefix: string,
|
|
update: string,
|
|
): string {
|
|
return `${prefix}mkdir -p ${shellQuote(
|
|
file.slice(0, file.lastIndexOf('/')),
|
|
)} && printf '%s' ${shellQuote(content)} | ${prefix}tee ${shellQuote(
|
|
file,
|
|
)} >/dev/null && ${update}`;
|
|
}
|