import hostedGitInfo from "hosted-git-info";
/**
 * Clone URLs are eventually handed to `git clone`. Even though the package
 * manager always spawns git with an argument array (never a shell), constrain
 * the composed clone URL to characters that cannot carry shell or option
 * meaning, so a parsed source can never smuggle a metacharacter downstream.
 */
const SAFE_CLONE_URL = /^[A-Za-z0-9._~:@\/%+-]+$/;
function splitRef(url) {
    const scpLikeMatch = url.match(/^git@([^:]+):(.+)$/);
    if (scpLikeMatch) {
        const pathWithMaybeRef = scpLikeMatch[2] ?? "";
        const refSeparator = pathWithMaybeRef.indexOf("@");
        if (refSeparator < 0)
            return { repo: url };
        const repoPath = pathWithMaybeRef.slice(0, refSeparator);
        const ref = pathWithMaybeRef.slice(refSeparator + 1);
        if (!repoPath || !ref)
            return { repo: url };
        return {
            repo: `git@${scpLikeMatch[1] ?? ""}:${repoPath}`,
            ref,
        };
    }
    if (url.includes("://")) {
        try {
            const parsed = new URL(url);
            const pathWithMaybeRef = parsed.pathname.replace(/^\/+/, "");
            const refSeparator = pathWithMaybeRef.indexOf("@");
            if (refSeparator < 0)
                return { repo: url };
            const repoPath = pathWithMaybeRef.slice(0, refSeparator);
            const ref = pathWithMaybeRef.slice(refSeparator + 1);
            if (!repoPath || !ref)
                return { repo: url };
            parsed.pathname = `/${repoPath}`;
            return {
                repo: parsed.toString().replace(/\/$/, ""),
                ref,
            };
        }
        catch {
            return { repo: url };
        }
    }
    const slashIndex = url.indexOf("/");
    if (slashIndex < 0) {
        return { repo: url };
    }
    const host = url.slice(0, slashIndex);
    const pathWithMaybeRef = url.slice(slashIndex + 1);
    const refSeparator = pathWithMaybeRef.indexOf("@");
    if (refSeparator < 0) {
        return { repo: url };
    }
    const repoPath = pathWithMaybeRef.slice(0, refSeparator);
    const ref = pathWithMaybeRef.slice(refSeparator + 1);
    if (!repoPath || !ref) {
        return { repo: url };
    }
    return {
        repo: `${host}/${repoPath}`,
        ref,
    };
}
function decodeForValidation(value) {
    try {
        return decodeURIComponent(value);
    }
    catch {
        return null;
    }
}
function hasUnsafeGitInstallPart(value, allowSlash) {
    const decoded = decodeForValidation(value);
    if (decoded === null) {
        return true;
    }
    const candidates = [value, decoded];
    for (const candidate of candidates) {
        if (candidate.includes("\0") || candidate.includes("\\") || candidate.startsWith("/")) {
            return true;
        }
        if (!allowSlash && candidate.includes("/")) {
            return true;
        }
        if (candidate.split("/").includes("..")) {
            return true;
        }
    }
    return false;
}
function buildGitSource(args) {
    if (args.path.startsWith("/")) {
        return null;
    }
    const normalizedPath = args.path.replace(/\.git$/, "").replace(/^\/+/, "");
    if (!args.host || !normalizedPath || normalizedPath.split("/").length < 2) {
        return null;
    }
    if (hasUnsafeGitInstallPart(args.host, false) || hasUnsafeGitInstallPart(normalizedPath, true)) {
        return null;
    }
    if (!SAFE_CLONE_URL.test(args.repo)) {
        return null;
    }
    return {
        type: "git",
        repo: args.repo,
        host: args.host,
        path: normalizedPath,
        ref: args.ref,
        pinned: Boolean(args.ref),
    };
}
function parseGenericGitUrl(url) {
    const { repo: repoWithoutRef, ref } = splitRef(url);
    let repo = repoWithoutRef;
    let host = "";
    let path = "";
    const scpLikeMatch = repoWithoutRef.match(/^git@([^:]+):(.+)$/);
    if (scpLikeMatch) {
        host = scpLikeMatch[1] ?? "";
        path = scpLikeMatch[2] ?? "";
    }
    else if (repoWithoutRef.startsWith("https://") ||
        repoWithoutRef.startsWith("http://") ||
        repoWithoutRef.startsWith("ssh://") ||
        repoWithoutRef.startsWith("git://")) {
        try {
            const parsed = new URL(repoWithoutRef);
            host = parsed.hostname;
            path = parsed.pathname.replace(/^\/+/, "");
        }
        catch {
            return null;
        }
    }
    else {
        const slashIndex = repoWithoutRef.indexOf("/");
        if (slashIndex < 0) {
            return null;
        }
        host = repoWithoutRef.slice(0, slashIndex);
        path = repoWithoutRef.slice(slashIndex + 1);
        if (!host.includes(".") && host !== "localhost") {
            return null;
        }
        repo = `https://${repoWithoutRef}`;
    }
    return buildGitSource({ repo, host, path, ref });
}
/**
 * Parse git source into a GitSource.
 *
 * Rules:
 * - With git: prefix, accept all historical shorthand forms.
 * - Without git: prefix, only accept explicit protocol URLs.
 */
export function parseGitUrl(source) {
    const trimmed = source.trim();
    const hasGitPrefix = trimmed.startsWith("git:");
    const url = hasGitPrefix ? trimmed.slice(4).trim() : trimmed;
    if (!hasGitPrefix && !/^(https?|ssh|git):\/\//i.test(url)) {
        return null;
    }
    const split = splitRef(url);
    const hostedCandidates = [split.ref ? `${split.repo}#${split.ref}` : undefined, url].filter((value) => Boolean(value));
    for (const candidate of hostedCandidates) {
        const info = hostedGitInfo.fromUrl(candidate);
        if (info) {
            if (split.ref && info.project?.includes("@")) {
                continue;
            }
            const useHttpsPrefix = !split.repo.startsWith("http://") &&
                !split.repo.startsWith("https://") &&
                !split.repo.startsWith("ssh://") &&
                !split.repo.startsWith("git://") &&
                !split.repo.startsWith("git@");
            return buildGitSource({
                repo: useHttpsPrefix ? `https://${split.repo}` : split.repo,
                host: info.domain || "",
                path: `${info.user}/${info.project}`,
                ref: info.committish || split.ref || undefined,
            });
        }
    }
    const httpsCandidates = [split.ref ? `https://${split.repo}#${split.ref}` : undefined, `https://${url}`].filter((value) => Boolean(value));
    for (const candidate of httpsCandidates) {
        const info = hostedGitInfo.fromUrl(candidate);
        if (info) {
            if (split.ref && info.project?.includes("@")) {
                continue;
            }
            return buildGitSource({
                repo: `https://${split.repo}`,
                host: info.domain || "",
                path: `${info.user}/${info.project}`,
                ref: info.committish || split.ref || undefined,
            });
        }
    }
    return parseGenericGitUrl(url);
}
//# sourceMappingURL=git.js.map