索引格式

以下定义了索引的格式。偶尔会添加新功能,这些功能仅从引入它们的 Cargo 版本开始才被理解。较旧的 Cargo 版本可能无法使用利用新功能的包。但是,旧包的格式不应更改,因此较旧的 Cargo 版本应该能够使用它们。

索引配置

索引的根目录包含一个名为 config.json 的文件,其中包含 Cargo 用于访问注册表的 JSON 信息。这是 crates.io 配置文件的一个示例

{
    "dl": "https://crates.io/api/v1/crates",
    "api": "https://crates.io"
}

键是

  • dl:这是下载索引中列出的 crate 的 URL。该值可能具有以下标记,这些标记将被替换为其对应的值

    • {crate}:crate 的名称。
    • {version}:crate 的版本。
    • {prefix}:从 crate 名称计算得出的目录前缀。例如,名为 cargo 的 crate 的前缀为 ca/rg。有关详细信息,请参见下文。
    • {lowerprefix}{prefix} 的小写变体。
    • {sha256-checksum}:crate 的 sha256 校验和。

    如果不存在任何标记,则将值 /{crate}/{version}/download 附加到末尾。

  • api:这是 Web API 的基本 URL。此键是可选的,但如果未指定,则诸如 cargo publish 之类的命令将无法工作。Web API 在下面描述。

  • auth-required:指示这是否是一个私有注册表,它要求所有操作都经过身份验证,包括 API 请求、crate 下载和稀疏索引更新。

下载端点

下载端点应发送所请求包的 .crate 文件。Cargo 支持 https、http 和 file URL、HTTP 重定向、HTTP1 和 HTTP2。TLS 支持的具体细节取决于运行 Cargo 的平台、Cargo 的版本以及编译方式。

如果在 config.json 中设置了 auth-required: true,则 HTTP(S) 下载请求将包含 Authorization 标头。

索引文件

索引存储库的其余部分包含每个包的一个文件,其中文件名是包的名称,以小写形式表示。包的每个版本在文件中都有单独的一行。这些文件组织在一个目录层中

  • 具有 1 个字符名称的包放置在名为 1 的目录中。
  • 具有 2 个字符名称的包放置在名为 2 的目录中。
  • 具有 3 个字符名称的包放置在目录 3/{first-character} 中,其中 {first-character} 是包名称的第一个字符。
  • 所有其他包都存储在名为 {first-two}/{second-two} 的目录中,其中顶级目录是包名称的前两个字符,下一个子目录是包名称的第三和第四个字符。例如,cargo 将存储在名为 ca/rg/cargo 的文件中。

注意:尽管索引文件名是小写的,但 Cargo.toml 和索引 JSON 数据中包含包名称的字段区分大小写,并且可能包含大写和小写字符。

上面的目录名称是根据转换为小写的包名称计算得出的;它由标记 {lowerprefix} 表示。当使用原始包名称而不进行大小写转换时,生成的目录名称由标记 {prefix} 表示。例如,包 MyCrate{prefix}My/Cr{lowerprefix}my/cr。一般来说,建议使用 {prefix} 而不是 {lowerprefix},但每种选择都有其优缺点。在不区分大小写的文件系统上使用 {prefix} 会导致(无害但不太优雅的)目录别名。例如,crateCrateTwo{prefix} 值分别为 cr/atCr/at;这些在 Unix 机器上是不同的,但在 Windows 上别名为同一目录。使用具有规范化大小写的目录可以避免别名,但在区分大小写的文件系统上,更难支持缺少 {prefix}/{lowerprefix} 的较旧版本的 Cargo。例如,nginx 重写规则可以轻松构造 {prefix},但无法执行大小写转换来构造 {lowerprefix}

名称限制

注册表应考虑对其索引中添加的包名称强制执行限制。Cargo 本身允许使用任何 字母数字-_ 字符的名称。crates.io 施加了自己的限制,包括以下内容

  • 只允许 ASCII 字符。
  • 仅限字母数字、-_ 字符。
  • 第一个字符必须是字母。
  • 不区分大小写的冲突检测。
  • 防止 -_ 的差异。
  • 在特定长度下(最大 64)。
  • 拒绝保留名称,例如 Windows 特殊文件名(如“nul”)。

注册表应考虑纳入类似的限制,并考虑安全性影响,例如 IDN 同形字攻击UTR36UTS39 中的其他问题。

版本唯一性

索引*必须*确保每个版本对于每个包只出现一次。这包括忽略 SemVer 构建元数据。例如,索引*不得*包含版本为 1.0.71.0.7+extra 的两个条目。

JSON 模式

包文件中的每一行都包含一个 JSON 对象,该对象描述了包的已发布版本。以下是一个漂亮打印的示例,其中包含注释,解释了条目的格式。

{
    // The name of the package.
    // This must only contain alphanumeric, `-`, or `_` characters.
    "name": "foo",
    // The version of the package this row is describing.
    // This must be a valid version number according to the Semantic
    // Versioning 2.0.0 spec at https://semver.org/.
    "vers": "0.1.0",
    // Array of direct dependencies of the package.
    "deps": [
        {
            // Name of the dependency.
            // If the dependency is renamed from the original package name,
            // this is the new name. The original package name is stored in
            // the `package` field.
            "name": "rand",
            // The SemVer requirement for this dependency.
            // This must be a valid version requirement defined at
            // https://doc.rust-lang.net.cn/cargo/reference/specifying-dependencies.html.
            "req": "^0.6",
            // Array of features (as strings) enabled for this dependency.
            // May be omitted since Cargo 1.84.
            "features": ["i128_support"],
            // Boolean of whether or not this is an optional dependency.
            // Since Cargo 1.84, defaults to `false` if not specified.
            "optional": false,
            // Boolean of whether or not default features are enabled.
            // Since Cargo 1.84, defaults to `true` if not specified.
            "default_features": true,
            // The target platform for the dependency.
            // If not specified or `null`, it is not a target dependency.
            // Otherwise, a string such as "cfg(windows)".
            "target": null,
            // The dependency kind.
            // "dev", "build", or "normal".
            // If not specified or `null`, it defaults to "normal".
            "kind": "normal",
            // The URL of the index of the registry where this dependency is
            // from as a string. If not specified or `null`, it is assumed the
            // dependency is in the current registry.
            "registry": null,
            // If the dependency is renamed, this is a string of the actual
            // package name. If not specified or `null`, this dependency is not
            // renamed.
            "package": null,
        }
    ],
    // A SHA256 checksum of the `.crate` file.
    "cksum": "d867001db0e2b6e0496f9fac96930e2d42233ecd3ca0413e0753d4c7695d289c",
    // Set of features defined for the package.
    // Each feature maps to an array of features or dependencies it enables.
    // May be omitted since Cargo 1.84.
    "features": {
        "extras": ["rand/simd_support"]
    },
    // Boolean of whether or not this version has been yanked.
    "yanked": false,
    // The `links` string value from the package's manifest, or null if not
    // specified. This field is optional and defaults to null.
    "links": null,
    // An unsigned 32-bit integer value indicating the schema version of this
    // entry.
    //
    // If this is not specified, it should be interpreted as the default of 1.
    //
    // Cargo (starting with version 1.51) will ignore versions it does not
    // recognize. This provides a method to safely introduce changes to index
    // entries and allow older versions of cargo to ignore newer entries it
    // doesn't understand. Versions older than 1.51 ignore this field, and
    // thus may misinterpret the meaning of the index entry.
    //
    // The current values are:
    //
    // * 1: The schema as documented here, not including newer additions.
    //      This is honored in Rust version 1.51 and newer.
    // * 2: The addition of the `features2` field.
    //      This is honored in Rust version 1.60 and newer.
    "v": 2,
    // This optional field contains features with new, extended syntax.
    // Specifically, namespaced features (`dep:`) and weak dependencies
    // (`pkg?/feat`).
    //
    // This is separated from `features` because versions older than 1.19
    // will fail to load due to not being able to parse the new syntax, even
    // with a `Cargo.lock` file.
    //
    // Cargo will merge any values listed here with the "features" field.
    //
    // If this field is included, the "v" field should be set to at least 2.
    //
    // Registries are not required to use this field for extended feature
    // syntax, they are allowed to include those in the "features" field.
    // Using this is only necessary if the registry wants to support cargo
    // versions older than 1.19, which in practice is only crates.io since
    // those older versions do not support other registries.
    "features2": {
        "serde": ["dep:serde", "chrono?/serde"]
    }
    // The minimal supported Rust version (optional)
    // This must be a valid version requirement without an operator (e.g. no `=`)
    "rust_version": "1.60"
}

JSON 对象在添加后不应修改,但 yanked 字段的值可能随时更改。

注意:索引 JSON 格式与 发布 APIcargo metadata 的 JSON 格式略有不同。如果你使用其中一个作为生成索引条目的来源,建议你仔细检查它们之间的文档差异。

对于 发布 API,差异是

  • deps
    • name — 当依赖项在 Cargo.toml重命名时,发布 API 将原始包名称放在 name 字段中,并将别名放在 explicit_name_in_toml 字段中。索引将别名放在 name 字段中,将原始包名称放在 package 字段中。
    • req — 发布 API 字段称为 version_req
  • cksum — 发布 API 不指定校验和,它必须由注册表在添加到索引之前计算。
  • features — 某些功能可能放置在 features2 字段中。注意:这只是 crates.io 的遗留要求;其他注册表无需费心修改功能映射。v 字段指示 features2 字段的存在。
  • 发布 API 包含其他几个字段,例如 descriptionreadme,它们不会出现在索引中。这些旨在使注册表更容易获得关于 crate 的元数据,以便在网站上显示,而无需提取和解析 .crate 文件。此附加信息通常添加到注册表服务器上的数据库中。
  • 尽管此处包含 rust_version,但 crates.io 将忽略此字段,而是从 .crate 文件中包含的 Cargo.toml 中读取它。

对于 cargo metadata,差异是

  • verscargo metadata 字段称为 version
  • deps
    • name — 当依赖项在 Cargo.toml重命名时,cargo metadata 将原始包名称放在 name 字段中,并将别名放在 rename 字段中。索引将别名放在 name 字段中,将原始包名称放在 package 字段中。
    • default_featurescargo metadata 字段称为 uses_default_features
    • registrycargo metadata 使用值 null 来表示依赖项来自 crates.io。索引使用值 null 来表示依赖项来自与索引相同的注册表。在创建索引条目时,crates.io 以外的注册表应将值 null 转换为 https://github.com/rust-lang/crates.io-index,并将与当前索引匹配的 URL 转换为 null
    • cargo metadata 包含一些额外的字段,例如 sourcepath
  • 索引包含额外的字段,例如 yankedcksumv

索引协议

Cargo 支持两种远程注册表协议:gitsparsegit 协议将索引文件存储在 git 存储库中,而 sparse 协议通过 HTTP 获取单个文件。

Git 协议

git 协议在索引 URL 中没有协议前缀。例如,crates.io 的 git 索引 URL 是 https://github.com/rust-lang/crates.io-index

Cargo 在磁盘上缓存 git 存储库,以便它可以有效地增量获取更新。

稀疏协议

稀疏协议在注册表 URL 中使用 sparse+ 协议前缀。例如,crates.io 的稀疏索引 URL 是 sparse+https://index.crates.io/

稀疏协议使用单独的 HTTP 请求下载每个索引文件。由于这会导致大量小的 HTTP 请求,因此支持流水线和 HTTP/2 的服务器可以显着提高性能。

稀疏身份验证

Cargo 将尝试在获取任何其他文件之前获取 config.json 文件。如果服务器响应 HTTP 401,则 Cargo 将假定注册表需要身份验证,并重新尝试请求包含身份验证令牌的 config.json

在身份验证失败(或缺少身份验证令牌)时,服务器可以包含一个 www-authenticate 标头,其中包含一个 Cargo login_url="<URL>" 质询,以指示用户可以去哪里获取令牌。

需要身份验证的注册表必须在 config.json 中设置 auth-required: true

缓存

Cargo 缓存 crate 元数据文件,并从服务器捕获每个条目的 ETagLast-Modified HTTP 标头。刷新 crate 元数据时,Cargo 会发送 If-None-MatchIf-Modified-Since 标头,以允许服务器在本地缓存有效时使用 HTTP 304“未修改”响应,从而节省时间和带宽。如果同时存在 ETagLast-Modified 标头,则 Cargo 仅使用 ETag

缓存失效

如果仓库使用了某种 CDN 或代理来缓存对索引文件的访问,那么建议仓库在文件更新时实施某种形式的缓存失效机制。如果这些缓存没有更新,用户可能无法访问新的 crate,直到缓存被清除。

不存在的 Crates

对于不存在的 crate,仓库应回复 404 “未找到”、410 “已删除” 或 451 “因法律原因不可用” 代码。

稀疏协议的限制

由于仓库的 URL 存储在 lockfile 中,因此不建议同时提供使用两种协议的仓库。关于转换计划的讨论正在 #10964 issue 中进行。 crates.io 仓库是一个例外,因为当使用稀疏协议时,Cargo 内部会替换为等效的 git URL。

如果仓库确实提供了两种协议,目前建议选择一种协议作为规范协议,并为另一种协议使用 源替换