|
| 1 | +import tarfile |
| 2 | + |
| 3 | +try: |
| 4 | + import tomllib |
| 5 | +except ModuleNotFoundError: |
| 6 | + import tomli as tomllib |
| 7 | + |
| 8 | + |
| 9 | +def extract_cargo_toml(crate_path, crate_name, version): |
| 10 | + """Extract and parse Cargo.toml from a .crate tarball.""" |
| 11 | + expected_path = f"{crate_name}-{version}/Cargo.toml" |
| 12 | + with tarfile.open(crate_path, "r:gz") as tar: |
| 13 | + cargo_toml_file = tar.extractfile(expected_path) |
| 14 | + if cargo_toml_file is None: |
| 15 | + raise FileNotFoundError(f"No Cargo.toml found in {crate_path} at {expected_path}") |
| 16 | + return tomllib.load(cargo_toml_file) |
| 17 | + |
| 18 | + |
| 19 | +def _normalize_req(version_str): |
| 20 | + """Normalize a Cargo version requirement to its explicit form. |
| 21 | +
|
| 22 | + In Cargo.toml, a bare version like "1.0" is shorthand for "^1.0". |
| 23 | + The index format uses the explicit form with the comparator prefix. |
| 24 | + """ |
| 25 | + if not version_str or version_str == "*": |
| 26 | + return version_str |
| 27 | + # Already has a comparator prefix |
| 28 | + if version_str[0] in ("^", "~", "=", ">", "<"): |
| 29 | + return version_str |
| 30 | + return f"^{version_str}" |
| 31 | + |
| 32 | + |
| 33 | +def parse_dep(name, spec, kind="normal", target=None): |
| 34 | + """Convert a single Cargo.toml dependency entry to index format.""" |
| 35 | + if isinstance(spec, str): |
| 36 | + # Simple form: dep = "1.0" |
| 37 | + return { |
| 38 | + "name": name, |
| 39 | + "req": _normalize_req(spec), |
| 40 | + "features": [], |
| 41 | + "optional": False, |
| 42 | + "default_features": True, |
| 43 | + "target": target, |
| 44 | + "kind": kind, |
| 45 | + "registry": None, |
| 46 | + "package": None, |
| 47 | + } |
| 48 | + |
| 49 | + # Table form: dep = { version = "1.0", optional = true, ... } |
| 50 | + dep = { |
| 51 | + "name": name, |
| 52 | + "req": _normalize_req(spec.get("version", "*")), |
| 53 | + "features": spec.get("features", []), |
| 54 | + "optional": spec.get("optional", False), |
| 55 | + "default_features": spec.get("default-features", True), |
| 56 | + "target": target, |
| 57 | + "kind": kind, |
| 58 | + "registry": spec.get("registry"), |
| 59 | + "package": None, |
| 60 | + } |
| 61 | + # If the dep was renamed, "name" in the index is the alias (the key), |
| 62 | + # and "package" is the real crate name |
| 63 | + if "package" in spec: |
| 64 | + dep["package"] = spec["package"] |
| 65 | + return dep |
| 66 | + |
| 67 | + |
| 68 | +def extract_dependencies(cargo_toml): |
| 69 | + """Extract all dependencies from a parsed Cargo.toml into index format.""" |
| 70 | + deps = [] |
| 71 | + |
| 72 | + for name, spec in cargo_toml.get("dependencies", {}).items(): |
| 73 | + deps.append(parse_dep(name, spec, kind="normal")) |
| 74 | + |
| 75 | + for name, spec in cargo_toml.get("dev-dependencies", {}).items(): |
| 76 | + deps.append(parse_dep(name, spec, kind="dev")) |
| 77 | + |
| 78 | + for name, spec in cargo_toml.get("build-dependencies", {}).items(): |
| 79 | + deps.append(parse_dep(name, spec, kind="build")) |
| 80 | + |
| 81 | + # Platform-specific dependencies: [target.'cfg(...)'.dependencies] |
| 82 | + for target, target_deps in cargo_toml.get("target", {}).items(): |
| 83 | + for name, spec in target_deps.get("dependencies", {}).items(): |
| 84 | + deps.append(parse_dep(name, spec, kind="normal", target=target)) |
| 85 | + for name, spec in target_deps.get("dev-dependencies", {}).items(): |
| 86 | + deps.append(parse_dep(name, spec, kind="dev", target=target)) |
| 87 | + for name, spec in target_deps.get("build-dependencies", {}).items(): |
| 88 | + deps.append(parse_dep(name, spec, kind="build", target=target)) |
| 89 | + |
| 90 | + return deps |
0 commit comments