cargo-util-schemas-0.7.0/.cargo_vcs_info.json0000644000000001670000000000100145330ustar { "git": { "sha1": "5ffbef3211a8c378857905775a15c5b32a174d3b" }, "path_in_vcs": "crates/cargo-util-schemas" }cargo-util-schemas-0.7.0/Cargo.toml0000644000000032500000000000100125250ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.81" name = "cargo-util-schemas" version = "0.7.0" build = false autobins = false autoexamples = false autotests = false autobenches = false description = "Deserialization schemas for Cargo" homepage = "https://github.com/rust-lang/cargo" readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/rust-lang/cargo" [lib] name = "cargo_util_schemas" path = "src/lib.rs" [dependencies.semver] version = "1.0.23" features = ["serde"] [dependencies.serde] version = "1.0.204" features = ["derive"] [dependencies.serde-untagged] version = "0.1.6" [dependencies.serde-value] version = "0.7.0" [dependencies.thiserror] version = "1.0.63" [dependencies.toml] version = "0.8.19" [dependencies.unicode-xid] version = "0.2.4" [dependencies.url] version = "2.5.2" [dev-dependencies.snapbox] version = "0.6.18" features = [ "diff", "dir", "term-svg", "regex", "json", ] [lints.clippy] dbg_macro = "warn" disallowed_methods = "warn" print_stderr = "warn" print_stdout = "warn" self_named_module_files = "warn" [lints.clippy.all] level = "allow" priority = -1 [lints.rust] rust_2018_idioms = "warn" [lints.rustdoc] private_intra_doc_links = "allow" cargo-util-schemas-0.7.0/Cargo.toml.orig000064400000000000000000000010651046102023000162100ustar 00000000000000[package] name = "cargo-util-schemas" version = "0.7.0" rust-version = "1.81" # MSRV:1 edition.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true description = "Deserialization schemas for Cargo" [dependencies] semver.workspace = true serde = { workspace = true, features = ["derive"] } serde-untagged.workspace = true serde-value.workspace = true thiserror.workspace = true toml.workspace = true unicode-xid.workspace = true url.workspace = true [lints] workspace = true [dev-dependencies] snapbox.workspace = true cargo-util-schemas-0.7.0/LICENSE-APACHE000064400000000000000000000251541046102023000152520ustar 00000000000000 Apache License Version 2.0, January 2004 https://www.apache.org/licenses/LICENSE-2.0 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. cargo-util-schemas-0.7.0/LICENSE-MIT000064400000000000000000000017771046102023000147670ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cargo-util-schemas-0.7.0/README.md000064400000000000000000000002051046102023000145730ustar 00000000000000> This crate is maintained by the Cargo team for use by the wider > ecosystem. This crate follows semver compatibility for its APIs. cargo-util-schemas-0.7.0/src/core/mod.rs000064400000000000000000000004541046102023000161660ustar 00000000000000mod package_id_spec; mod partial_version; mod source_kind; pub use package_id_spec::PackageIdSpec; pub use package_id_spec::PackageIdSpecError; pub use partial_version::PartialVersion; pub use partial_version::PartialVersionError; pub use source_kind::GitReference; pub use source_kind::SourceKind; cargo-util-schemas-0.7.0/src/core/package_id_spec.rs000064400000000000000000000627241046102023000205000ustar 00000000000000use std::fmt; use semver::Version; use serde::{de, ser}; use url::Url; use crate::core::GitReference; use crate::core::PartialVersion; use crate::core::PartialVersionError; use crate::core::SourceKind; use crate::manifest::PackageName; use crate::restricted_names::NameValidationError; type Result = std::result::Result; /// Some or all of the data required to identify a package: /// /// 1. the package name (a `String`, required) /// 2. the package version (a `Version`, optional) /// 3. the package source (a `Url`, optional) /// /// If any of the optional fields are omitted, then the package ID may be ambiguous, there may be /// more than one package/version/url combo that will match. However, often just the name is /// sufficient to uniquely define a package ID. #[derive(Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)] pub struct PackageIdSpec { name: String, version: Option, url: Option, kind: Option, } impl PackageIdSpec { pub fn new(name: String) -> Self { Self { name, version: None, url: None, kind: None, } } pub fn with_version(mut self, version: PartialVersion) -> Self { self.version = Some(version); self } pub fn with_url(mut self, url: Url) -> Self { self.url = Some(url); self } pub fn with_kind(mut self, kind: SourceKind) -> Self { self.kind = Some(kind); self } /// Parses a spec string and returns a `PackageIdSpec` if the string was valid. /// /// # Examples /// Some examples of valid strings /// /// ``` /// use cargo_util_schemas::core::PackageIdSpec; /// /// let specs = vec![ /// "foo", /// "foo@1.4", /// "foo@1.4.3", /// "foo:1.2.3", /// "https://github.com/rust-lang/crates.io-index#foo", /// "https://github.com/rust-lang/crates.io-index#foo@1.4.3", /// "ssh://git@github.com/rust-lang/foo.git#foo@1.4.3", /// "file:///path/to/my/project/foo", /// "file:///path/to/my/project/foo#1.1.8" /// ]; /// for spec in specs { /// assert!(PackageIdSpec::parse(spec).is_ok()); /// } pub fn parse(spec: &str) -> Result { if spec.contains("://") { if let Ok(url) = Url::parse(spec) { return PackageIdSpec::from_url(url); } } else if spec.contains('/') || spec.contains('\\') { let abs = std::env::current_dir().unwrap_or_default().join(spec); if abs.exists() { let maybe_url = Url::from_file_path(abs) .map_or_else(|_| "a file:// URL".to_string(), |url| url.to_string()); return Err(ErrorKind::MaybeFilePath { spec: spec.into(), maybe_url, } .into()); } } let (name, version) = parse_spec(spec)?.unwrap_or_else(|| (spec.to_owned(), None)); PackageName::new(&name)?; Ok(PackageIdSpec { name: String::from(name), version, url: None, kind: None, }) } /// Tries to convert a valid `Url` to a `PackageIdSpec`. fn from_url(mut url: Url) -> Result { let mut kind = None; if let Some((kind_str, scheme)) = url.scheme().split_once('+') { match kind_str { "git" => { let git_ref = GitReference::from_query(url.query_pairs()); url.set_query(None); kind = Some(SourceKind::Git(git_ref)); url = strip_url_protocol(&url); } "registry" => { if url.query().is_some() { return Err(ErrorKind::UnexpectedQueryString(url).into()); } kind = Some(SourceKind::Registry); url = strip_url_protocol(&url); } "sparse" => { if url.query().is_some() { return Err(ErrorKind::UnexpectedQueryString(url).into()); } kind = Some(SourceKind::SparseRegistry); // Leave `sparse` as part of URL, see `SourceId::new` // url = strip_url_protocol(&url); } "path" => { if url.query().is_some() { return Err(ErrorKind::UnexpectedQueryString(url).into()); } if scheme != "file" { return Err(ErrorKind::UnsupportedPathPlusScheme(scheme.into()).into()); } kind = Some(SourceKind::Path); url = strip_url_protocol(&url); } kind => return Err(ErrorKind::UnsupportedProtocol(kind.into()).into()), } } else { if url.query().is_some() { return Err(ErrorKind::UnexpectedQueryString(url).into()); } } let frag = url.fragment().map(|s| s.to_owned()); url.set_fragment(None); let (name, version) = { let Some(path_name) = url.path_segments().and_then(|mut p| p.next_back()) else { return Err(ErrorKind::MissingUrlPath(url).into()); }; match frag { Some(fragment) => match parse_spec(&fragment)? { Some((name, ver)) => (name, ver), None => { if fragment.chars().next().unwrap().is_alphabetic() { (String::from(fragment.as_str()), None) } else { let version = fragment.parse::()?; (String::from(path_name), Some(version)) } } }, None => (String::from(path_name), None), } }; PackageName::new(&name)?; Ok(PackageIdSpec { name, version, url: Some(url), kind, }) } pub fn name(&self) -> &str { self.name.as_str() } /// Full `semver::Version`, if present pub fn version(&self) -> Option { self.version.as_ref().and_then(|v| v.to_version()) } pub fn partial_version(&self) -> Option<&PartialVersion> { self.version.as_ref() } pub fn url(&self) -> Option<&Url> { self.url.as_ref() } pub fn set_url(&mut self, url: Url) { self.url = Some(url); } pub fn kind(&self) -> Option<&SourceKind> { self.kind.as_ref() } pub fn set_kind(&mut self, kind: SourceKind) { self.kind = Some(kind); } } fn parse_spec(spec: &str) -> Result)>> { let Some((name, ver)) = spec .rsplit_once('@') .or_else(|| spec.rsplit_once(':').filter(|(n, _)| !n.ends_with(':'))) else { return Ok(None); }; let name = name.to_owned(); let ver = ver.parse::()?; Ok(Some((name, Some(ver)))) } fn strip_url_protocol(url: &Url) -> Url { // Ridiculous hoop because `Url::set_scheme` errors when changing to http/https let raw = url.to_string(); raw.split_once('+').unwrap().1.parse().unwrap() } impl fmt::Display for PackageIdSpec { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut printed_name = false; match self.url { Some(ref url) => { if let Some(protocol) = self.kind.as_ref().and_then(|k| k.protocol()) { write!(f, "{protocol}+")?; } write!(f, "{}", url)?; if let Some(SourceKind::Git(git_ref)) = self.kind.as_ref() { if let Some(pretty) = git_ref.pretty_ref(true) { write!(f, "?{}", pretty)?; } } if url.path_segments().unwrap().next_back().unwrap() != &*self.name { printed_name = true; write!(f, "#{}", self.name)?; } } None => { printed_name = true; write!(f, "{}", self.name)?; } } if let Some(ref v) = self.version { write!(f, "{}{}", if printed_name { "@" } else { "#" }, v)?; } Ok(()) } } impl ser::Serialize for PackageIdSpec { fn serialize(&self, s: S) -> std::result::Result where S: ser::Serializer, { self.to_string().serialize(s) } } impl<'de> de::Deserialize<'de> for PackageIdSpec { fn deserialize(d: D) -> std::result::Result where D: de::Deserializer<'de>, { let string = String::deserialize(d)?; PackageIdSpec::parse(&string).map_err(de::Error::custom) } } /// Error parsing a [`PackageIdSpec`]. #[derive(Debug, thiserror::Error)] #[error(transparent)] pub struct PackageIdSpecError(#[from] ErrorKind); impl From for PackageIdSpecError { fn from(value: PartialVersionError) -> Self { ErrorKind::PartialVersion(value).into() } } impl From for PackageIdSpecError { fn from(value: NameValidationError) -> Self { ErrorKind::NameValidation(value).into() } } /// Non-public error kind for [`PackageIdSpecError`]. #[non_exhaustive] #[derive(Debug, thiserror::Error)] enum ErrorKind { #[error("unsupported source protocol: {0}")] UnsupportedProtocol(String), #[error("`path+{0}` is unsupported; `path+file` and `file` schemes are supported")] UnsupportedPathPlusScheme(String), #[error("cannot have a query string in a pkgid: {0}")] UnexpectedQueryString(Url), #[error("pkgid urls must have at least one path component: {0}")] MissingUrlPath(Url), #[error("package ID specification `{spec}` looks like a file path, maybe try {maybe_url}")] MaybeFilePath { spec: String, maybe_url: String }, #[error(transparent)] NameValidation(#[from] crate::restricted_names::NameValidationError), #[error(transparent)] PartialVersion(#[from] crate::core::PartialVersionError), } #[cfg(test)] mod tests { use super::ErrorKind; use super::PackageIdSpec; use crate::core::{GitReference, SourceKind}; use url::Url; #[track_caller] fn ok(spec: &str, expected: PackageIdSpec, expected_rendered: &str) { let parsed = PackageIdSpec::parse(spec).unwrap(); assert_eq!(parsed, expected); let rendered = parsed.to_string(); assert_eq!(rendered, expected_rendered); let reparsed = PackageIdSpec::parse(&rendered).unwrap(); assert_eq!(reparsed, expected); } macro_rules! err { ($spec:expr, $expected:pat) => { let err = PackageIdSpec::parse($spec).unwrap_err(); let kind = err.0; assert!( matches!(kind, $expected), "`{}` parse error mismatch, got {kind:?}", $spec ); }; } #[test] fn good_parsing() { ok( "https://crates.io/foo", PackageIdSpec { name: String::from("foo"), version: None, url: Some(Url::parse("https://crates.io/foo").unwrap()), kind: None, }, "https://crates.io/foo", ); ok( "https://crates.io/foo#1.2.3", PackageIdSpec { name: String::from("foo"), version: Some("1.2.3".parse().unwrap()), url: Some(Url::parse("https://crates.io/foo").unwrap()), kind: None, }, "https://crates.io/foo#1.2.3", ); ok( "https://crates.io/foo#1.2", PackageIdSpec { name: String::from("foo"), version: Some("1.2".parse().unwrap()), url: Some(Url::parse("https://crates.io/foo").unwrap()), kind: None, }, "https://crates.io/foo#1.2", ); ok( "https://crates.io/foo#bar:1.2.3", PackageIdSpec { name: String::from("bar"), version: Some("1.2.3".parse().unwrap()), url: Some(Url::parse("https://crates.io/foo").unwrap()), kind: None, }, "https://crates.io/foo#bar@1.2.3", ); ok( "https://crates.io/foo#bar@1.2.3", PackageIdSpec { name: String::from("bar"), version: Some("1.2.3".parse().unwrap()), url: Some(Url::parse("https://crates.io/foo").unwrap()), kind: None, }, "https://crates.io/foo#bar@1.2.3", ); ok( "https://crates.io/foo#bar@1.2", PackageIdSpec { name: String::from("bar"), version: Some("1.2".parse().unwrap()), url: Some(Url::parse("https://crates.io/foo").unwrap()), kind: None, }, "https://crates.io/foo#bar@1.2", ); ok( "registry+https://crates.io/foo#bar@1.2", PackageIdSpec { name: String::from("bar"), version: Some("1.2".parse().unwrap()), url: Some(Url::parse("https://crates.io/foo").unwrap()), kind: Some(SourceKind::Registry), }, "registry+https://crates.io/foo#bar@1.2", ); ok( "sparse+https://crates.io/foo#bar@1.2", PackageIdSpec { name: String::from("bar"), version: Some("1.2".parse().unwrap()), url: Some(Url::parse("sparse+https://crates.io/foo").unwrap()), kind: Some(SourceKind::SparseRegistry), }, "sparse+https://crates.io/foo#bar@1.2", ); ok( "foo", PackageIdSpec { name: String::from("foo"), version: None, url: None, kind: None, }, "foo", ); ok( "foo::bar", PackageIdSpec { name: String::from("foo::bar"), version: None, url: None, kind: None, }, "foo::bar", ); ok( "foo:1.2.3", PackageIdSpec { name: String::from("foo"), version: Some("1.2.3".parse().unwrap()), url: None, kind: None, }, "foo@1.2.3", ); ok( "foo::bar:1.2.3", PackageIdSpec { name: String::from("foo::bar"), version: Some("1.2.3".parse().unwrap()), url: None, kind: None, }, "foo::bar@1.2.3", ); ok( "foo@1.2.3", PackageIdSpec { name: String::from("foo"), version: Some("1.2.3".parse().unwrap()), url: None, kind: None, }, "foo@1.2.3", ); ok( "foo::bar@1.2.3", PackageIdSpec { name: String::from("foo::bar"), version: Some("1.2.3".parse().unwrap()), url: None, kind: None, }, "foo::bar@1.2.3", ); ok( "foo@1.2", PackageIdSpec { name: String::from("foo"), version: Some("1.2".parse().unwrap()), url: None, kind: None, }, "foo@1.2", ); // pkgid-spec.md ok( "regex", PackageIdSpec { name: String::from("regex"), version: None, url: None, kind: None, }, "regex", ); ok( "regex@1.4", PackageIdSpec { name: String::from("regex"), version: Some("1.4".parse().unwrap()), url: None, kind: None, }, "regex@1.4", ); ok( "regex@1.4.3", PackageIdSpec { name: String::from("regex"), version: Some("1.4.3".parse().unwrap()), url: None, kind: None, }, "regex@1.4.3", ); ok( "https://github.com/rust-lang/crates.io-index#regex", PackageIdSpec { name: String::from("regex"), version: None, url: Some(Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()), kind: None, }, "https://github.com/rust-lang/crates.io-index#regex", ); ok( "https://github.com/rust-lang/crates.io-index#regex@1.4.3", PackageIdSpec { name: String::from("regex"), version: Some("1.4.3".parse().unwrap()), url: Some(Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()), kind: None, }, "https://github.com/rust-lang/crates.io-index#regex@1.4.3", ); ok( "sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3", PackageIdSpec { name: String::from("regex"), version: Some("1.4.3".parse().unwrap()), url: Some( Url::parse("sparse+https://github.com/rust-lang/crates.io-index").unwrap(), ), kind: Some(SourceKind::SparseRegistry), }, "sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3", ); ok( "https://github.com/rust-lang/cargo#0.52.0", PackageIdSpec { name: String::from("cargo"), version: Some("0.52.0".parse().unwrap()), url: Some(Url::parse("https://github.com/rust-lang/cargo").unwrap()), kind: None, }, "https://github.com/rust-lang/cargo#0.52.0", ); ok( "https://github.com/rust-lang/cargo#cargo-platform@0.1.2", PackageIdSpec { name: String::from("cargo-platform"), version: Some("0.1.2".parse().unwrap()), url: Some(Url::parse("https://github.com/rust-lang/cargo").unwrap()), kind: None, }, "https://github.com/rust-lang/cargo#cargo-platform@0.1.2", ); ok( "ssh://git@github.com/rust-lang/regex.git#regex@1.4.3", PackageIdSpec { name: String::from("regex"), version: Some("1.4.3".parse().unwrap()), url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()), kind: None, }, "ssh://git@github.com/rust-lang/regex.git#regex@1.4.3", ); ok( "git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3", PackageIdSpec { name: String::from("regex"), version: Some("1.4.3".parse().unwrap()), url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()), kind: Some(SourceKind::Git(GitReference::DefaultBranch)), }, "git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3", ); ok( "git+ssh://git@github.com/rust-lang/regex.git?branch=dev#regex@1.4.3", PackageIdSpec { name: String::from("regex"), version: Some("1.4.3".parse().unwrap()), url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()), kind: Some(SourceKind::Git(GitReference::Branch("dev".to_owned()))), }, "git+ssh://git@github.com/rust-lang/regex.git?branch=dev#regex@1.4.3", ); ok( "file:///path/to/my/project/foo", PackageIdSpec { name: String::from("foo"), version: None, url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()), kind: None, }, "file:///path/to/my/project/foo", ); ok( "file:///path/to/my/project/foo::bar", PackageIdSpec { name: String::from("foo::bar"), version: None, url: Some(Url::parse("file:///path/to/my/project/foo::bar").unwrap()), kind: None, }, "file:///path/to/my/project/foo::bar", ); ok( "file:///path/to/my/project/foo#1.1.8", PackageIdSpec { name: String::from("foo"), version: Some("1.1.8".parse().unwrap()), url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()), kind: None, }, "file:///path/to/my/project/foo#1.1.8", ); ok( "path+file:///path/to/my/project/foo#1.1.8", PackageIdSpec { name: String::from("foo"), version: Some("1.1.8".parse().unwrap()), url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()), kind: Some(SourceKind::Path), }, "path+file:///path/to/my/project/foo#1.1.8", ); ok( "path+file:///path/to/my/project/foo#bar", PackageIdSpec { name: String::from("bar"), version: None, url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()), kind: Some(SourceKind::Path), }, "path+file:///path/to/my/project/foo#bar", ); ok( "path+file:///path/to/my/project/foo#foo::bar", PackageIdSpec { name: String::from("foo::bar"), version: None, url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()), kind: Some(SourceKind::Path), }, "path+file:///path/to/my/project/foo#foo::bar", ); ok( "path+file:///path/to/my/project/foo#bar:1.1.8", PackageIdSpec { name: String::from("bar"), version: Some("1.1.8".parse().unwrap()), url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()), kind: Some(SourceKind::Path), }, "path+file:///path/to/my/project/foo#bar@1.1.8", ); ok( "path+file:///path/to/my/project/foo#foo::bar:1.1.8", PackageIdSpec { name: String::from("foo::bar"), version: Some("1.1.8".parse().unwrap()), url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()), kind: Some(SourceKind::Path), }, "path+file:///path/to/my/project/foo#foo::bar@1.1.8", ); ok( "path+file:///path/to/my/project/foo#bar@1.1.8", PackageIdSpec { name: String::from("bar"), version: Some("1.1.8".parse().unwrap()), url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()), kind: Some(SourceKind::Path), }, "path+file:///path/to/my/project/foo#bar@1.1.8", ); ok( "path+file:///path/to/my/project/foo#foo::bar@1.1.8", PackageIdSpec { name: String::from("foo::bar"), version: Some("1.1.8".parse().unwrap()), url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()), kind: Some(SourceKind::Path), }, "path+file:///path/to/my/project/foo#foo::bar@1.1.8", ); } #[test] fn bad_parsing() { err!("baz:", ErrorKind::PartialVersion(_)); err!("baz:*", ErrorKind::PartialVersion(_)); err!("baz@", ErrorKind::PartialVersion(_)); err!("baz@*", ErrorKind::PartialVersion(_)); err!("baz@^1.0", ErrorKind::PartialVersion(_)); err!("https://baz:1.0", ErrorKind::NameValidation(_)); err!("https://#baz:1.0", ErrorKind::NameValidation(_)); err!( "foobar+https://github.com/rust-lang/crates.io-index", ErrorKind::UnsupportedProtocol(_) ); err!( "path+https://github.com/rust-lang/crates.io-index", ErrorKind::UnsupportedPathPlusScheme(_) ); // Only `git+` can use `?` err!( "file:///path/to/my/project/foo?branch=dev", ErrorKind::UnexpectedQueryString(_) ); err!( "path+file:///path/to/my/project/foo?branch=dev", ErrorKind::UnexpectedQueryString(_) ); err!( "registry+https://github.com/rust-lang/cargo?branch=dev#0.52.0", ErrorKind::UnexpectedQueryString(_) ); err!( "sparse+https://github.com/rust-lang/cargo?branch=dev#0.52.0", ErrorKind::UnexpectedQueryString(_) ); err!("@1.2.3", ErrorKind::NameValidation(_)); err!("registry+https://github.com", ErrorKind::NameValidation(_)); err!("https://crates.io/1foo#1.2.3", ErrorKind::NameValidation(_)); } } cargo-util-schemas-0.7.0/src/core/partial_version.rs000064400000000000000000000174031046102023000206120ustar 00000000000000use std::fmt::{self, Display}; use semver::{Comparator, Version, VersionReq}; use serde_untagged::UntaggedEnumVisitor; #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] pub struct PartialVersion { pub major: u64, pub minor: Option, pub patch: Option, pub pre: Option, pub build: Option, } impl PartialVersion { pub fn to_version(&self) -> Option { Some(Version { major: self.major, minor: self.minor?, patch: self.patch?, pre: self.pre.clone().unwrap_or_default(), build: self.build.clone().unwrap_or_default(), }) } pub fn to_caret_req(&self) -> VersionReq { VersionReq { comparators: vec![Comparator { op: semver::Op::Caret, major: self.major, minor: self.minor, patch: self.patch, pre: self.pre.as_ref().cloned().unwrap_or_default(), }], } } /// Check if this matches a version, including build metadata /// /// Build metadata does not affect version precedence but may be necessary for uniquely /// identifying a package. pub fn matches(&self, version: &Version) -> bool { if !version.pre.is_empty() && self.pre.is_none() { // Pre-release versions must be explicitly opted into, if for no other reason than to // give us room to figure out and define the semantics return false; } self.major == version.major && self.minor.map(|f| f == version.minor).unwrap_or(true) && self.patch.map(|f| f == version.patch).unwrap_or(true) && self.pre.as_ref().map(|f| f == &version.pre).unwrap_or(true) && self .build .as_ref() .map(|f| f == &version.build) .unwrap_or(true) } } impl From for PartialVersion { fn from(ver: semver::Version) -> Self { let pre = if ver.pre.is_empty() { None } else { Some(ver.pre) }; let build = if ver.build.is_empty() { None } else { Some(ver.build) }; Self { major: ver.major, minor: Some(ver.minor), patch: Some(ver.patch), pre, build, } } } impl std::str::FromStr for PartialVersion { type Err = PartialVersionError; fn from_str(value: &str) -> Result { match semver::Version::parse(value) { Ok(ver) => Ok(ver.into()), Err(_) => { // HACK: Leverage `VersionReq` for partial version parsing let mut version_req = match semver::VersionReq::parse(value) { Ok(req) => req, Err(_) if value.contains('-') => return Err(ErrorKind::Prerelease.into()), Err(_) if value.contains('+') => return Err(ErrorKind::BuildMetadata.into()), Err(_) => return Err(ErrorKind::Unexpected.into()), }; if version_req.comparators.len() != 1 { return Err(ErrorKind::VersionReq.into()); } let comp = version_req.comparators.pop().unwrap(); if comp.op != semver::Op::Caret { return Err(ErrorKind::VersionReq.into()); } else if value.starts_with('^') { // Can't distinguish between `^` present or not return Err(ErrorKind::VersionReq.into()); } let pre = if comp.pre.is_empty() { None } else { Some(comp.pre) }; Ok(Self { major: comp.major, minor: comp.minor, patch: comp.patch, pre, build: None, }) } } } } impl Display for PartialVersion { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let major = self.major; write!(f, "{major}")?; if let Some(minor) = self.minor { write!(f, ".{minor}")?; } if let Some(patch) = self.patch { write!(f, ".{patch}")?; } if let Some(pre) = self.pre.as_ref() { write!(f, "-{pre}")?; } if let Some(build) = self.build.as_ref() { write!(f, "+{build}")?; } Ok(()) } } impl serde::Serialize for PartialVersion { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.collect_str(self) } } impl<'de> serde::Deserialize<'de> for PartialVersion { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { UntaggedEnumVisitor::new() .expecting("SemVer version") .string(|value| value.parse().map_err(serde::de::Error::custom)) .deserialize(deserializer) } } /// Error parsing a [`PartialVersion`]. #[derive(Debug, thiserror::Error)] #[error(transparent)] pub struct PartialVersionError(#[from] ErrorKind); /// Non-public error kind for [`PartialVersionError`]. #[non_exhaustive] #[derive(Debug, thiserror::Error)] enum ErrorKind { #[error("unexpected version requirement, expected a version like \"1.32\"")] VersionReq, #[error("unexpected prerelease field, expected a version like \"1.32\"")] Prerelease, #[error("unexpected build field, expected a version like \"1.32\"")] BuildMetadata, #[error("expected a version like \"1.32\"")] Unexpected, } #[cfg(test)] mod test { use super::*; use snapbox::prelude::*; use snapbox::str; #[test] fn parse_success() { let cases = &[ // Valid pre-release ("1.43.0-beta.1", str!["1.43.0-beta.1"]), // Valid pre-release with wildcard ("1.43.0-beta.1.x", str!["1.43.0-beta.1.x"]), ]; for (input, expected) in cases { let actual: Result = input.parse(); let actual = match actual { Ok(result) => result.to_string(), Err(err) => format!("didn't pass: {err}"), }; snapbox::assert_data_eq!(actual, expected.clone().raw()); } } #[test] fn parse_errors() { let cases = &[ // Disallow caret ( "^1.43", str![[r#"unexpected version requirement, expected a version like "1.32""#]], ), // Bad pre-release ( "1.43-beta.1", str![[r#"unexpected prerelease field, expected a version like "1.32""#]], ), // Weird wildcard ( "x", str![[r#"unexpected version requirement, expected a version like "1.32""#]], ), ( "1.x", str![[r#"unexpected version requirement, expected a version like "1.32""#]], ), ( "1.1.x", str![[r#"unexpected version requirement, expected a version like "1.32""#]], ), // Non-sense ("foodaddle", str![[r#"expected a version like "1.32""#]]), ]; for (input, expected) in cases { let actual: Result = input.parse(); let actual = match actual { Ok(result) => format!("didn't fail: {result:?}"), Err(err) => err.to_string(), }; snapbox::assert_data_eq!(actual, expected.clone().raw()); } } } cargo-util-schemas-0.7.0/src/core/source_kind.rs000064400000000000000000000173171046102023000177220ustar 00000000000000use std::cmp::Ordering; /// The possible kinds of code source. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum SourceKind { /// A git repository. Git(GitReference), /// A local path. Path, /// A remote registry. Registry, /// A sparse registry. SparseRegistry, /// A local filesystem-based registry. LocalRegistry, /// A directory-based registry. Directory, } impl SourceKind { pub fn protocol(&self) -> Option<&str> { match self { SourceKind::Path => Some("path"), SourceKind::Git(_) => Some("git"), SourceKind::Registry => Some("registry"), // Sparse registry URL already includes the `sparse+` prefix, see `SourceId::new` SourceKind::SparseRegistry => None, SourceKind::LocalRegistry => Some("local-registry"), SourceKind::Directory => Some("directory"), } } } /// Note that this is specifically not derived on `SourceKind` although the /// implementation here is very similar to what it might look like if it were /// otherwise derived. /// /// The reason for this is somewhat obtuse. First of all the hash value of /// `SourceKind` makes its way into `~/.cargo/registry/index/github.com-XXXX` /// which means that changes to the hash means that all Rust users need to /// redownload the crates.io index and all their crates. If possible we strive /// to not change this to make this redownloading behavior happen as little as /// possible. How is this connected to `Ord` you might ask? That's a good /// question! /// /// Since the beginning of time `SourceKind` has had `#[derive(Hash)]`. It for /// the longest time *also* derived the `Ord` and `PartialOrd` traits. In #8522, /// however, the implementation of `Ord` changed. This handwritten implementation /// forgot to sync itself with the originally derived implementation, namely /// placing git dependencies as sorted after all other dependencies instead of /// first as before. /// /// This regression in #8522 (Rust 1.47) went unnoticed. When we switched back /// to a derived implementation in #9133 (Rust 1.52 beta) we only then ironically /// saw an issue (#9334). In #9334 it was observed that stable Rust at the time /// (1.51) was sorting git dependencies last, whereas Rust 1.52 beta would sort /// git dependencies first. This is because the `PartialOrd` implementation in /// 1.51 used #8522, the buggy implementation, which put git deps last. In 1.52 /// it was (unknowingly) restored to the pre-1.47 behavior with git dependencies /// first. /// /// Because the breakage was only witnessed after the original breakage, this /// trait implementation is preserving the "broken" behavior. Put a different way: /// /// * Rust pre-1.47 sorted git deps first. /// * Rust 1.47 to Rust 1.51 sorted git deps last, a breaking change (#8522) that /// was never noticed. /// * Rust 1.52 restored the pre-1.47 behavior (#9133, without knowing it did /// so), and breakage was witnessed by actual users due to difference with /// 1.51. /// * Rust 1.52 (the source as it lives now) was fixed to match the 1.47-1.51 /// behavior (#9383), which is now considered intentionally breaking from the /// pre-1.47 behavior. /// /// Note that this was all discovered when Rust 1.53 was in nightly and 1.52 was /// in beta. #9133 was in both beta and nightly at the time of discovery. For /// 1.52 #9383 reverted #9133, meaning 1.52 is the same as 1.51. On nightly /// (1.53) #9397 was created to fix the regression introduced by #9133 relative /// to the current stable (1.51). /// /// That's all a long winded way of saying "it's weird that git deps hash first /// and are sorted last, but it's the way it is right now". The author of this /// comment chose to handwrite the `Ord` implementation instead of the `Hash` /// implementation, but it's only required that at most one of them is /// hand-written because the other can be derived. Perhaps one day in /// the future someone can figure out how to remove this behavior. impl Ord for SourceKind { fn cmp(&self, other: &SourceKind) -> Ordering { match (self, other) { (SourceKind::Path, SourceKind::Path) => Ordering::Equal, (SourceKind::Path, _) => Ordering::Less, (_, SourceKind::Path) => Ordering::Greater, (SourceKind::Registry, SourceKind::Registry) => Ordering::Equal, (SourceKind::Registry, _) => Ordering::Less, (_, SourceKind::Registry) => Ordering::Greater, (SourceKind::SparseRegistry, SourceKind::SparseRegistry) => Ordering::Equal, (SourceKind::SparseRegistry, _) => Ordering::Less, (_, SourceKind::SparseRegistry) => Ordering::Greater, (SourceKind::LocalRegistry, SourceKind::LocalRegistry) => Ordering::Equal, (SourceKind::LocalRegistry, _) => Ordering::Less, (_, SourceKind::LocalRegistry) => Ordering::Greater, (SourceKind::Directory, SourceKind::Directory) => Ordering::Equal, (SourceKind::Directory, _) => Ordering::Less, (_, SourceKind::Directory) => Ordering::Greater, (SourceKind::Git(a), SourceKind::Git(b)) => a.cmp(b), } } } /// Forwards to `Ord` impl PartialOrd for SourceKind { fn partial_cmp(&self, other: &SourceKind) -> Option { Some(self.cmp(other)) } } /// Information to find a specific commit in a Git repository. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum GitReference { /// From a tag. Tag(String), /// From a branch. Branch(String), /// From a specific revision. Can be a commit hash (either short or full), /// or a named reference like `refs/pull/493/head`. Rev(String), /// The default branch of the repository, the reference named `HEAD`. DefaultBranch, } impl GitReference { pub fn from_query( query_pairs: impl Iterator, impl AsRef)>, ) -> Self { let mut reference = GitReference::DefaultBranch; for (k, v) in query_pairs { let v = v.as_ref(); match k.as_ref() { // Map older 'ref' to branch. "branch" | "ref" => reference = GitReference::Branch(v.to_owned()), "rev" => reference = GitReference::Rev(v.to_owned()), "tag" => reference = GitReference::Tag(v.to_owned()), _ => {} } } reference } /// Returns a `Display`able view of this git reference, or None if using /// the head of the default branch pub fn pretty_ref(&self, url_encoded: bool) -> Option> { match self { GitReference::DefaultBranch => None, _ => Some(PrettyRef { inner: self, url_encoded, }), } } } /// A git reference that can be `Display`ed pub struct PrettyRef<'a> { inner: &'a GitReference, url_encoded: bool, } impl<'a> std::fmt::Display for PrettyRef<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let value: &str; match self.inner { GitReference::Branch(s) => { write!(f, "branch=")?; value = s; } GitReference::Tag(s) => { write!(f, "tag=")?; value = s; } GitReference::Rev(s) => { write!(f, "rev=")?; value = s; } GitReference::DefaultBranch => unreachable!(), } if self.url_encoded { for value in url::form_urlencoded::byte_serialize(value.as_bytes()) { write!(f, "{value}")?; } } else { write!(f, "{value}")?; } Ok(()) } } cargo-util-schemas-0.7.0/src/lib.rs000064400000000000000000000007461046102023000152310ustar 00000000000000//! Low-level Cargo format schemas //! //! This is types with logic mostly focused on `serde` and `FromStr` for use in reading files and //! parsing command-lines. //! Any logic for getting final semantics from these will likely need other tools to process, like //! `cargo metadata`. //! //! > This crate is maintained by the Cargo team for use by the wider //! > ecosystem. This crate follows semver compatibility for its APIs. pub mod core; pub mod manifest; mod restricted_names; cargo-util-schemas-0.7.0/src/manifest/mod.rs000064400000000000000000001523501046102023000170470ustar 00000000000000//! `Cargo.toml` / Manifest schema definition //! //! ## Style //! //! - Fields duplicated for an alias will have an accessor with the primary field's name //! - Keys that exist for bookkeeping but don't correspond to the schema have a `_` prefix use std::collections::BTreeMap; use std::collections::BTreeSet; use std::fmt::{self, Display, Write}; use std::path::PathBuf; use std::str; use serde::de::{self, IntoDeserializer as _, Unexpected}; use serde::ser; use serde::{Deserialize, Serialize}; use serde_untagged::UntaggedEnumVisitor; use crate::core::PackageIdSpec; use crate::restricted_names; mod rust_version; pub use crate::restricted_names::NameValidationError; pub use rust_version::RustVersion; pub use rust_version::RustVersionError; /// This type is used to deserialize `Cargo.toml` files. #[derive(Default, Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct TomlManifest { // when adding new fields, be sure to check whether `requires_package` should disallow them pub cargo_features: Option>, pub package: Option>, pub project: Option>, pub profile: Option, pub lib: Option, pub bin: Option>, pub example: Option>, pub test: Option>, pub bench: Option>, pub dependencies: Option>, pub dev_dependencies: Option>, #[serde(rename = "dev_dependencies")] pub dev_dependencies2: Option>, pub build_dependencies: Option>, #[serde(rename = "build_dependencies")] pub build_dependencies2: Option>, pub features: Option>>, pub target: Option>, pub replace: Option>, pub patch: Option>>, pub workspace: Option, pub badges: Option>>, pub lints: Option, /// Report unused keys (see also nested `_unused_keys`) /// Note: this is populated by the caller, rather than automatically #[serde(skip)] pub _unused_keys: BTreeSet, } impl TomlManifest { pub fn requires_package(&self) -> impl Iterator { [ self.lib.as_ref().map(|_| "lib"), self.bin.as_ref().map(|_| "bin"), self.example.as_ref().map(|_| "example"), self.test.as_ref().map(|_| "test"), self.bench.as_ref().map(|_| "bench"), self.dependencies.as_ref().map(|_| "dependencies"), self.dev_dependencies().as_ref().map(|_| "dev-dependencies"), self.build_dependencies() .as_ref() .map(|_| "build-dependencies"), self.features.as_ref().map(|_| "features"), self.target.as_ref().map(|_| "target"), self.badges.as_ref().map(|_| "badges"), self.lints.as_ref().map(|_| "lints"), ] .into_iter() .flatten() } pub fn has_profiles(&self) -> bool { self.profile.is_some() } pub fn package(&self) -> Option<&Box> { self.package.as_ref().or(self.project.as_ref()) } pub fn dev_dependencies(&self) -> Option<&BTreeMap> { self.dev_dependencies .as_ref() .or(self.dev_dependencies2.as_ref()) } pub fn build_dependencies(&self) -> Option<&BTreeMap> { self.build_dependencies .as_ref() .or(self.build_dependencies2.as_ref()) } pub fn features(&self) -> Option<&BTreeMap>> { self.features.as_ref() } pub fn normalized_lints(&self) -> Result, UnresolvedError> { self.lints.as_ref().map(|l| l.normalized()).transpose() } } #[derive(Debug, Default, Deserialize, Serialize, Clone)] #[serde(rename_all = "kebab-case")] pub struct TomlWorkspace { pub members: Option>, pub exclude: Option>, pub default_members: Option>, pub resolver: Option, pub metadata: Option, // Properties that can be inherited by members. pub package: Option, pub dependencies: Option>, pub lints: Option, } /// A group of fields that are inheritable by members of the workspace #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct InheritablePackage { pub version: Option, pub authors: Option>, pub description: Option, pub homepage: Option, pub documentation: Option, pub readme: Option, pub keywords: Option>, pub categories: Option>, pub license: Option, pub license_file: Option, pub repository: Option, pub publish: Option, pub edition: Option, pub badges: Option>>, pub exclude: Option>, pub include: Option>, pub rust_version: Option, } /// Represents the `package`/`project` sections of a `Cargo.toml`. /// /// Note that the order of the fields matters, since this is the order they /// are serialized to a TOML file. For example, you cannot have values after /// the field `metadata`, since it is a table and values cannot appear after /// tables. #[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "kebab-case")] pub struct TomlPackage { pub edition: Option, pub rust_version: Option, pub name: PackageName, pub version: Option, pub authors: Option, pub build: Option, pub metabuild: Option, pub default_target: Option, pub forced_target: Option, pub links: Option, pub exclude: Option, pub include: Option, pub publish: Option, pub workspace: Option, pub im_a_teapot: Option, pub autolib: Option, pub autobins: Option, pub autoexamples: Option, pub autotests: Option, pub autobenches: Option, pub default_run: Option, // Package metadata. pub description: Option, pub homepage: Option, pub documentation: Option, pub readme: Option, pub keywords: Option, pub categories: Option, pub license: Option, pub license_file: Option, pub repository: Option, pub resolver: Option, pub metadata: Option, /// Provide a helpful error message for a common user error. #[serde(rename = "cargo-features", skip_serializing)] pub _invalid_cargo_features: Option, } impl TomlPackage { pub fn new(name: PackageName) -> Self { Self { name, edition: None, rust_version: None, version: None, authors: None, build: None, metabuild: None, default_target: None, forced_target: None, links: None, exclude: None, include: None, publish: None, workspace: None, im_a_teapot: None, autolib: None, autobins: None, autoexamples: None, autotests: None, autobenches: None, default_run: None, description: None, homepage: None, documentation: None, readme: None, keywords: None, categories: None, license: None, license_file: None, repository: None, resolver: None, metadata: None, _invalid_cargo_features: None, } } pub fn normalized_edition(&self) -> Result, UnresolvedError> { self.edition.as_ref().map(|v| v.normalized()).transpose() } pub fn normalized_rust_version(&self) -> Result, UnresolvedError> { self.rust_version .as_ref() .map(|v| v.normalized()) .transpose() } pub fn normalized_version(&self) -> Result, UnresolvedError> { self.version.as_ref().map(|v| v.normalized()).transpose() } pub fn normalized_authors(&self) -> Result>, UnresolvedError> { self.authors.as_ref().map(|v| v.normalized()).transpose() } pub fn normalized_build(&self) -> Result, UnresolvedError> { let readme = self.build.as_ref().ok_or(UnresolvedError)?; match readme { StringOrBool::Bool(false) => Ok(None), StringOrBool::Bool(true) => Err(UnresolvedError), StringOrBool::String(value) => Ok(Some(value)), } } pub fn normalized_exclude(&self) -> Result>, UnresolvedError> { self.exclude.as_ref().map(|v| v.normalized()).transpose() } pub fn normalized_include(&self) -> Result>, UnresolvedError> { self.include.as_ref().map(|v| v.normalized()).transpose() } pub fn normalized_publish(&self) -> Result, UnresolvedError> { self.publish.as_ref().map(|v| v.normalized()).transpose() } pub fn normalized_description(&self) -> Result, UnresolvedError> { self.description .as_ref() .map(|v| v.normalized()) .transpose() } pub fn normalized_homepage(&self) -> Result, UnresolvedError> { self.homepage.as_ref().map(|v| v.normalized()).transpose() } pub fn normalized_documentation(&self) -> Result, UnresolvedError> { self.documentation .as_ref() .map(|v| v.normalized()) .transpose() } pub fn normalized_readme(&self) -> Result, UnresolvedError> { let readme = self.readme.as_ref().ok_or(UnresolvedError)?; readme.normalized().and_then(|sb| match sb { StringOrBool::Bool(false) => Ok(None), StringOrBool::Bool(true) => Err(UnresolvedError), StringOrBool::String(value) => Ok(Some(value)), }) } pub fn normalized_keywords(&self) -> Result>, UnresolvedError> { self.keywords.as_ref().map(|v| v.normalized()).transpose() } pub fn normalized_categories(&self) -> Result>, UnresolvedError> { self.categories.as_ref().map(|v| v.normalized()).transpose() } pub fn normalized_license(&self) -> Result, UnresolvedError> { self.license.as_ref().map(|v| v.normalized()).transpose() } pub fn normalized_license_file(&self) -> Result, UnresolvedError> { self.license_file .as_ref() .map(|v| v.normalized()) .transpose() } pub fn normalized_repository(&self) -> Result, UnresolvedError> { self.repository.as_ref().map(|v| v.normalized()).transpose() } } /// An enum that allows for inheriting keys from a workspace in a Cargo.toml. #[derive(Serialize, Copy, Clone, Debug)] #[serde(untagged)] pub enum InheritableField { /// The type that is used when not inheriting from a workspace. Value(T), /// The type when inheriting from a workspace. Inherit(TomlInheritedField), } impl InheritableField { pub fn normalized(&self) -> Result<&T, UnresolvedError> { self.as_value().ok_or(UnresolvedError) } pub fn as_value(&self) -> Option<&T> { match self { InheritableField::Inherit(_) => None, InheritableField::Value(defined) => Some(defined), } } } //. This already has a `Deserialize` impl from version_trim_whitespace pub type InheritableSemverVersion = InheritableField; impl<'de> de::Deserialize<'de> for InheritableSemverVersion { fn deserialize(d: D) -> Result where D: de::Deserializer<'de>, { UntaggedEnumVisitor::new() .expecting("SemVer version") .string( |value| match value.trim().parse().map_err(de::Error::custom) { Ok(parsed) => Ok(InheritableField::Value(parsed)), Err(e) => Err(e), }, ) .map(|value| value.deserialize().map(InheritableField::Inherit)) .deserialize(d) } } pub type InheritableString = InheritableField; impl<'de> de::Deserialize<'de> for InheritableString { fn deserialize(d: D) -> Result where D: de::Deserializer<'de>, { struct Visitor; impl<'de> de::Visitor<'de> for Visitor { type Value = InheritableString; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { f.write_str("a string or workspace") } fn visit_string(self, value: String) -> Result where E: de::Error, { Ok(InheritableString::Value(value)) } fn visit_map(self, map: V) -> Result where V: de::MapAccess<'de>, { let mvd = de::value::MapAccessDeserializer::new(map); TomlInheritedField::deserialize(mvd).map(InheritableField::Inherit) } } d.deserialize_any(Visitor) } } pub type InheritableRustVersion = InheritableField; impl<'de> de::Deserialize<'de> for InheritableRustVersion { fn deserialize(d: D) -> Result where D: de::Deserializer<'de>, { struct Visitor; impl<'de> de::Visitor<'de> for Visitor { type Value = InheritableRustVersion; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { f.write_str("a semver or workspace") } fn visit_string(self, value: String) -> Result where E: de::Error, { let value = value.parse::().map_err(|e| E::custom(e))?; Ok(InheritableRustVersion::Value(value)) } fn visit_map(self, map: V) -> Result where V: de::MapAccess<'de>, { let mvd = de::value::MapAccessDeserializer::new(map); TomlInheritedField::deserialize(mvd).map(InheritableField::Inherit) } } d.deserialize_any(Visitor) } } pub type InheritableVecString = InheritableField>; impl<'de> de::Deserialize<'de> for InheritableVecString { fn deserialize(d: D) -> Result where D: de::Deserializer<'de>, { struct Visitor; impl<'de> de::Visitor<'de> for Visitor { type Value = InheritableVecString; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { f.write_str("a vector of strings or workspace") } fn visit_seq(self, v: A) -> Result where A: de::SeqAccess<'de>, { let seq = de::value::SeqAccessDeserializer::new(v); Vec::deserialize(seq).map(InheritableField::Value) } fn visit_map(self, map: V) -> Result where V: de::MapAccess<'de>, { let mvd = de::value::MapAccessDeserializer::new(map); TomlInheritedField::deserialize(mvd).map(InheritableField::Inherit) } } d.deserialize_any(Visitor) } } pub type InheritableStringOrBool = InheritableField; impl<'de> de::Deserialize<'de> for InheritableStringOrBool { fn deserialize(d: D) -> Result where D: de::Deserializer<'de>, { struct Visitor; impl<'de> de::Visitor<'de> for Visitor { type Value = InheritableStringOrBool; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { f.write_str("a string, a bool, or workspace") } fn visit_bool(self, v: bool) -> Result where E: de::Error, { let b = de::value::BoolDeserializer::new(v); StringOrBool::deserialize(b).map(InheritableField::Value) } fn visit_string(self, v: String) -> Result where E: de::Error, { let string = de::value::StringDeserializer::new(v); StringOrBool::deserialize(string).map(InheritableField::Value) } fn visit_map(self, map: V) -> Result where V: de::MapAccess<'de>, { let mvd = de::value::MapAccessDeserializer::new(map); TomlInheritedField::deserialize(mvd).map(InheritableField::Inherit) } } d.deserialize_any(Visitor) } } pub type InheritableVecStringOrBool = InheritableField; impl<'de> de::Deserialize<'de> for InheritableVecStringOrBool { fn deserialize(d: D) -> Result where D: de::Deserializer<'de>, { struct Visitor; impl<'de> de::Visitor<'de> for Visitor { type Value = InheritableVecStringOrBool; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { f.write_str("a boolean, a vector of strings, or workspace") } fn visit_bool(self, v: bool) -> Result where E: de::Error, { let b = de::value::BoolDeserializer::new(v); VecStringOrBool::deserialize(b).map(InheritableField::Value) } fn visit_seq(self, v: A) -> Result where A: de::SeqAccess<'de>, { let seq = de::value::SeqAccessDeserializer::new(v); VecStringOrBool::deserialize(seq).map(InheritableField::Value) } fn visit_map(self, map: V) -> Result where V: de::MapAccess<'de>, { let mvd = de::value::MapAccessDeserializer::new(map); TomlInheritedField::deserialize(mvd).map(InheritableField::Inherit) } } d.deserialize_any(Visitor) } } pub type InheritableBtreeMap = InheritableField>>; impl<'de> de::Deserialize<'de> for InheritableBtreeMap { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { let value = serde_value::Value::deserialize(deserializer)?; if let Ok(w) = TomlInheritedField::deserialize( serde_value::ValueDeserializer::::new(value.clone()), ) { return Ok(InheritableField::Inherit(w)); } BTreeMap::deserialize(serde_value::ValueDeserializer::::new(value)) .map(InheritableField::Value) } } #[derive(Deserialize, Serialize, Copy, Clone, Debug)] #[serde(rename_all = "kebab-case")] pub struct TomlInheritedField { workspace: WorkspaceValue, } impl TomlInheritedField { pub fn new() -> Self { TomlInheritedField { workspace: WorkspaceValue, } } } impl Default for TomlInheritedField { fn default() -> Self { Self::new() } } #[derive(Deserialize, Serialize, Copy, Clone, Debug)] #[serde(try_from = "bool")] #[serde(into = "bool")] struct WorkspaceValue; impl TryFrom for WorkspaceValue { type Error = String; fn try_from(other: bool) -> Result { if other { Ok(WorkspaceValue) } else { Err("`workspace` cannot be false".to_owned()) } } } impl From for bool { fn from(_: WorkspaceValue) -> bool { true } } #[derive(Serialize, Clone, Debug)] #[serde(untagged)] pub enum InheritableDependency { /// The type that is used when not inheriting from a workspace. Value(TomlDependency), /// The type when inheriting from a workspace. Inherit(TomlInheritedDependency), } impl InheritableDependency { pub fn unused_keys(&self) -> Vec { match self { InheritableDependency::Value(d) => d.unused_keys(), InheritableDependency::Inherit(w) => w._unused_keys.keys().cloned().collect(), } } pub fn normalized(&self) -> Result<&TomlDependency, UnresolvedError> { match self { InheritableDependency::Value(d) => Ok(d), InheritableDependency::Inherit(_) => Err(UnresolvedError), } } } impl<'de> de::Deserialize<'de> for InheritableDependency { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { let value = serde_value::Value::deserialize(deserializer)?; if let Ok(w) = TomlInheritedDependency::deserialize(serde_value::ValueDeserializer::< D::Error, >::new(value.clone())) { return if w.workspace { Ok(InheritableDependency::Inherit(w)) } else { Err(de::Error::custom("`workspace` cannot be false")) }; } TomlDependency::deserialize(serde_value::ValueDeserializer::::new(value)) .map(InheritableDependency::Value) } } #[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "kebab-case")] pub struct TomlInheritedDependency { pub workspace: bool, pub features: Option>, pub default_features: Option, #[serde(rename = "default_features")] pub default_features2: Option, pub optional: Option, pub public: Option, /// This is here to provide a way to see the "unused manifest keys" when deserializing #[serde(skip_serializing)] #[serde(flatten)] pub _unused_keys: BTreeMap, } impl TomlInheritedDependency { pub fn default_features(&self) -> Option { self.default_features.or(self.default_features2) } } #[derive(Clone, Debug, Serialize)] #[serde(untagged)] pub enum TomlDependency { /// In the simple format, only a version is specified, eg. /// `package = ""` Simple(String), /// The simple format is equivalent to a detailed dependency /// specifying only a version, eg. /// `package = { version = "" }` Detailed(TomlDetailedDependency

), } impl TomlDependency { pub fn is_version_specified(&self) -> bool { match self { TomlDependency::Detailed(d) => d.version.is_some(), TomlDependency::Simple(..) => true, } } pub fn is_optional(&self) -> bool { match self { TomlDependency::Detailed(d) => d.optional.unwrap_or(false), TomlDependency::Simple(..) => false, } } pub fn is_public(&self) -> bool { match self { TomlDependency::Detailed(d) => d.public.unwrap_or(false), TomlDependency::Simple(..) => false, } } pub fn default_features(&self) -> Option { match self { TomlDependency::Detailed(d) => d.default_features(), TomlDependency::Simple(..) => None, } } pub fn unused_keys(&self) -> Vec { match self { TomlDependency::Simple(_) => vec![], TomlDependency::Detailed(detailed) => detailed._unused_keys.keys().cloned().collect(), } } } impl<'de, P: Deserialize<'de> + Clone> de::Deserialize<'de> for TomlDependency

{ fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { UntaggedEnumVisitor::new() .expecting( "a version string like \"0.9.8\" or a \ detailed dependency like { version = \"0.9.8\" }", ) .string(|value| Ok(TomlDependency::Simple(value.to_owned()))) .map(|value| value.deserialize().map(TomlDependency::Detailed)) .deserialize(deserializer) } } #[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "kebab-case")] pub struct TomlDetailedDependency { pub version: Option, pub registry: Option, /// The URL of the `registry` field. /// This is an internal implementation detail. When Cargo creates a /// package, it replaces `registry` with `registry-index` so that the /// manifest contains the correct URL. All users won't have the same /// registry names configured, so Cargo can't rely on just the name for /// crates published by other users. pub registry_index: Option, // `path` is relative to the file it appears in. If that's a `Cargo.toml`, it'll be relative to // that TOML file, and if it's a `.cargo/config` file, it'll be relative to that file. pub path: Option

, pub base: Option, pub git: Option, pub branch: Option, pub tag: Option, pub rev: Option, pub features: Option>, pub optional: Option, pub default_features: Option, #[serde(rename = "default_features")] pub default_features2: Option, pub package: Option, pub public: Option, /// One or more of `bin`, `cdylib`, `staticlib`, `bin:`. pub artifact: Option, /// If set, the artifact should also be a dependency pub lib: Option, /// A platform name, like `x86_64-apple-darwin` pub target: Option, /// This is here to provide a way to see the "unused manifest keys" when deserializing #[serde(skip_serializing)] #[serde(flatten)] pub _unused_keys: BTreeMap, } impl TomlDetailedDependency

{ pub fn default_features(&self) -> Option { self.default_features.or(self.default_features2) } } // Explicit implementation so we avoid pulling in P: Default impl Default for TomlDetailedDependency

{ fn default() -> Self { Self { version: Default::default(), registry: Default::default(), registry_index: Default::default(), path: Default::default(), base: Default::default(), git: Default::default(), branch: Default::default(), tag: Default::default(), rev: Default::default(), features: Default::default(), optional: Default::default(), default_features: Default::default(), default_features2: Default::default(), package: Default::default(), public: Default::default(), artifact: Default::default(), lib: Default::default(), target: Default::default(), _unused_keys: Default::default(), } } } #[derive(Deserialize, Serialize, Clone, Debug, Default)] pub struct TomlProfiles(pub BTreeMap); impl TomlProfiles { pub fn get_all(&self) -> &BTreeMap { &self.0 } pub fn get(&self, name: &str) -> Option<&TomlProfile> { self.0.get(name) } } #[derive(Deserialize, Serialize, Clone, Debug, Default, Eq, PartialEq)] #[serde(default, rename_all = "kebab-case")] pub struct TomlProfile { pub opt_level: Option, pub lto: Option, pub codegen_backend: Option, pub codegen_units: Option, pub debug: Option, pub split_debuginfo: Option, pub debug_assertions: Option, pub rpath: Option, pub panic: Option, pub overflow_checks: Option, pub incremental: Option, pub dir_name: Option, pub inherits: Option, pub strip: Option, // Note that `rustflags` is used for the cargo-feature `profile_rustflags` pub rustflags: Option>, // These two fields must be last because they are sub-tables, and TOML // requires all non-tables to be listed first. pub package: Option>, pub build_override: Option>, /// Unstable feature `-Ztrim-paths`. pub trim_paths: Option, } impl TomlProfile { /// Overwrite self's values with the given profile. pub fn merge(&mut self, profile: &Self) { if let Some(v) = &profile.opt_level { self.opt_level = Some(v.clone()); } if let Some(v) = &profile.lto { self.lto = Some(v.clone()); } if let Some(v) = &profile.codegen_backend { self.codegen_backend = Some(v.clone()); } if let Some(v) = profile.codegen_units { self.codegen_units = Some(v); } if let Some(v) = profile.debug { self.debug = Some(v); } if let Some(v) = profile.debug_assertions { self.debug_assertions = Some(v); } if let Some(v) = &profile.split_debuginfo { self.split_debuginfo = Some(v.clone()); } if let Some(v) = profile.rpath { self.rpath = Some(v); } if let Some(v) = &profile.panic { self.panic = Some(v.clone()); } if let Some(v) = profile.overflow_checks { self.overflow_checks = Some(v); } if let Some(v) = profile.incremental { self.incremental = Some(v); } if let Some(v) = &profile.rustflags { self.rustflags = Some(v.clone()); } if let Some(other_package) = &profile.package { match &mut self.package { Some(self_package) => { for (spec, other_pkg_profile) in other_package { match self_package.get_mut(spec) { Some(p) => p.merge(other_pkg_profile), None => { self_package.insert(spec.clone(), other_pkg_profile.clone()); } } } } None => self.package = Some(other_package.clone()), } } if let Some(other_bo) = &profile.build_override { match &mut self.build_override { Some(self_bo) => self_bo.merge(other_bo), None => self.build_override = Some(other_bo.clone()), } } if let Some(v) = &profile.inherits { self.inherits = Some(v.clone()); } if let Some(v) = &profile.dir_name { self.dir_name = Some(v.clone()); } if let Some(v) = &profile.strip { self.strip = Some(v.clone()); } if let Some(v) = &profile.trim_paths { self.trim_paths = Some(v.clone()) } } } #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] pub enum ProfilePackageSpec { Spec(PackageIdSpec), All, } impl fmt::Display for ProfilePackageSpec { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ProfilePackageSpec::Spec(spec) => spec.fmt(f), ProfilePackageSpec::All => f.write_str("*"), } } } impl ser::Serialize for ProfilePackageSpec { fn serialize(&self, s: S) -> Result where S: ser::Serializer, { self.to_string().serialize(s) } } impl<'de> de::Deserialize<'de> for ProfilePackageSpec { fn deserialize(d: D) -> Result where D: de::Deserializer<'de>, { let string = String::deserialize(d)?; if string == "*" { Ok(ProfilePackageSpec::All) } else { PackageIdSpec::parse(&string) .map_err(de::Error::custom) .map(ProfilePackageSpec::Spec) } } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct TomlOptLevel(pub String); impl ser::Serialize for TomlOptLevel { fn serialize(&self, serializer: S) -> Result where S: ser::Serializer, { match self.0.parse::() { Ok(n) => n.serialize(serializer), Err(_) => self.0.serialize(serializer), } } } impl<'de> de::Deserialize<'de> for TomlOptLevel { fn deserialize(d: D) -> Result where D: de::Deserializer<'de>, { use serde::de::Error as _; UntaggedEnumVisitor::new() .expecting("an optimization level") .i64(|value| Ok(TomlOptLevel(value.to_string()))) .string(|value| { if value == "s" || value == "z" { Ok(TomlOptLevel(value.to_string())) } else { Err(serde_untagged::de::Error::custom(format!( "must be `0`, `1`, `2`, `3`, `s` or `z`, \ but found the string: \"{}\"", value ))) } }) .deserialize(d) } } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] pub enum TomlDebugInfo { None, LineDirectivesOnly, LineTablesOnly, Limited, Full, } impl Display for TomlDebugInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { TomlDebugInfo::None => f.write_char('0'), TomlDebugInfo::Limited => f.write_char('1'), TomlDebugInfo::Full => f.write_char('2'), TomlDebugInfo::LineDirectivesOnly => f.write_str("line-directives-only"), TomlDebugInfo::LineTablesOnly => f.write_str("line-tables-only"), } } } impl ser::Serialize for TomlDebugInfo { fn serialize(&self, serializer: S) -> Result where S: ser::Serializer, { match self { Self::None => 0.serialize(serializer), Self::LineDirectivesOnly => "line-directives-only".serialize(serializer), Self::LineTablesOnly => "line-tables-only".serialize(serializer), Self::Limited => 1.serialize(serializer), Self::Full => 2.serialize(serializer), } } } impl<'de> de::Deserialize<'de> for TomlDebugInfo { fn deserialize(d: D) -> Result where D: de::Deserializer<'de>, { use serde::de::Error as _; let expecting = "a boolean, 0, 1, 2, \"none\", \"limited\", \"full\", \"line-tables-only\", or \"line-directives-only\""; UntaggedEnumVisitor::new() .expecting(expecting) .bool(|value| { Ok(if value { TomlDebugInfo::Full } else { TomlDebugInfo::None }) }) .i64(|value| { let debuginfo = match value { 0 => TomlDebugInfo::None, 1 => TomlDebugInfo::Limited, 2 => TomlDebugInfo::Full, _ => { return Err(serde_untagged::de::Error::invalid_value( Unexpected::Signed(value), &expecting, )) } }; Ok(debuginfo) }) .string(|value| { let debuginfo = match value { "none" => TomlDebugInfo::None, "limited" => TomlDebugInfo::Limited, "full" => TomlDebugInfo::Full, "line-directives-only" => TomlDebugInfo::LineDirectivesOnly, "line-tables-only" => TomlDebugInfo::LineTablesOnly, _ => { return Err(serde_untagged::de::Error::invalid_value( Unexpected::Str(value), &expecting, )) } }; Ok(debuginfo) }) .deserialize(d) } } #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize)] #[serde(untagged, rename_all = "kebab-case")] pub enum TomlTrimPaths { Values(Vec), All, } impl TomlTrimPaths { pub fn none() -> Self { TomlTrimPaths::Values(Vec::new()) } pub fn is_none(&self) -> bool { match self { TomlTrimPaths::Values(v) => v.is_empty(), TomlTrimPaths::All => false, } } } impl<'de> de::Deserialize<'de> for TomlTrimPaths { fn deserialize(d: D) -> Result where D: de::Deserializer<'de>, { use serde::de::Error as _; let expecting = r#"a boolean, "none", "diagnostics", "macro", "object", "all", or an array with these options"#; UntaggedEnumVisitor::new() .expecting(expecting) .bool(|value| { Ok(if value { TomlTrimPaths::All } else { TomlTrimPaths::none() }) }) .string(|v| match v { "none" => Ok(TomlTrimPaths::none()), "all" => Ok(TomlTrimPaths::All), v => { let d = v.into_deserializer(); let err = |_: D::Error| { serde_untagged::de::Error::custom(format!("expected {expecting}")) }; TomlTrimPathsValue::deserialize(d) .map_err(err) .map(|v| v.into()) } }) .seq(|seq| { let seq: Vec = seq.deserialize()?; let seq: Vec<_> = seq .into_iter() .map(|s| TomlTrimPathsValue::deserialize(s.into_deserializer())) .collect::>()?; Ok(seq.into()) }) .deserialize(d) } } impl fmt::Display for TomlTrimPaths { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { TomlTrimPaths::All => write!(f, "all"), TomlTrimPaths::Values(v) if v.is_empty() => write!(f, "none"), TomlTrimPaths::Values(v) => { let mut iter = v.iter(); if let Some(value) = iter.next() { write!(f, "{value}")?; } for value in iter { write!(f, ",{value}")?; } Ok(()) } } } } impl From for TomlTrimPaths { fn from(value: TomlTrimPathsValue) -> Self { TomlTrimPaths::Values(vec![value]) } } impl From> for TomlTrimPaths { fn from(value: Vec) -> Self { TomlTrimPaths::Values(value) } } #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum TomlTrimPathsValue { Diagnostics, Macro, Object, } impl TomlTrimPathsValue { pub fn as_str(&self) -> &'static str { match self { TomlTrimPathsValue::Diagnostics => "diagnostics", TomlTrimPathsValue::Macro => "macro", TomlTrimPathsValue::Object => "object", } } } impl fmt::Display for TomlTrimPathsValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.as_str()) } } pub type TomlLibTarget = TomlTarget; pub type TomlBinTarget = TomlTarget; pub type TomlExampleTarget = TomlTarget; pub type TomlTestTarget = TomlTarget; pub type TomlBenchTarget = TomlTarget; #[derive(Default, Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct TomlTarget { pub name: Option, // The intention was to only accept `crate-type` here but historical // versions of Cargo also accepted `crate_type`, so look for both. pub crate_type: Option>, #[serde(rename = "crate_type")] pub crate_type2: Option>, pub path: Option, // Note that `filename` is used for the cargo-feature `different_binary_name` pub filename: Option, pub test: Option, pub doctest: Option, pub bench: Option, pub doc: Option, pub doc_scrape_examples: Option, pub proc_macro: Option, #[serde(rename = "proc_macro")] pub proc_macro2: Option, pub harness: Option, pub required_features: Option>, pub edition: Option, } impl TomlTarget { pub fn new() -> TomlTarget { TomlTarget::default() } pub fn proc_macro(&self) -> Option { self.proc_macro.or(self.proc_macro2).or_else(|| { if let Some(types) = self.crate_types() { if types.contains(&"proc-macro".to_string()) { return Some(true); } } None }) } pub fn crate_types(&self) -> Option<&Vec> { self.crate_type .as_ref() .or_else(|| self.crate_type2.as_ref()) } } macro_rules! str_newtype { ($name:ident) => { /// Verified string newtype #[derive(Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[serde(transparent)] pub struct $name = String>(T); impl> $name { pub fn into_inner(self) -> T { self.0 } } impl> AsRef for $name { fn as_ref(&self) -> &str { self.0.as_ref() } } impl> std::ops::Deref for $name { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } impl> std::borrow::Borrow for $name { fn borrow(&self) -> &str { self.0.as_ref() } } impl<'a> std::str::FromStr for $name { type Err = restricted_names::NameValidationError; fn from_str(value: &str) -> Result { Self::new(value.to_owned()) } } impl<'de, T: AsRef + serde::Deserialize<'de>> serde::Deserialize<'de> for $name { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let inner = T::deserialize(deserializer)?; Self::new(inner).map_err(serde::de::Error::custom) } } impl> Display for $name { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.as_ref().fmt(f) } } }; } str_newtype!(PackageName); impl> PackageName { /// Validated package name pub fn new(name: T) -> Result { restricted_names::validate_package_name(name.as_ref())?; Ok(Self(name)) } } impl PackageName { /// Coerce a value to be a validate package name /// /// Replaces invalid values with `placeholder` pub fn sanitize(name: impl AsRef, placeholder: char) -> Self { PackageName(restricted_names::sanitize_package_name( name.as_ref(), placeholder, )) } } str_newtype!(RegistryName); impl> RegistryName { /// Validated registry name pub fn new(name: T) -> Result { restricted_names::validate_registry_name(name.as_ref())?; Ok(Self(name)) } } str_newtype!(ProfileName); impl> ProfileName { /// Validated profile name pub fn new(name: T) -> Result { restricted_names::validate_profile_name(name.as_ref())?; Ok(Self(name)) } } str_newtype!(FeatureName); impl> FeatureName { /// Validated feature name pub fn new(name: T) -> Result { restricted_names::validate_feature_name(name.as_ref())?; Ok(Self(name)) } } str_newtype!(PathBaseName); impl> PathBaseName { /// Validated path base name pub fn new(name: T) -> Result { restricted_names::validate_path_base_name(name.as_ref())?; Ok(Self(name)) } } /// Corresponds to a `target` entry, but `TomlTarget` is already used. #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct TomlPlatform { pub dependencies: Option>, pub build_dependencies: Option>, #[serde(rename = "build_dependencies")] pub build_dependencies2: Option>, pub dev_dependencies: Option>, #[serde(rename = "dev_dependencies")] pub dev_dependencies2: Option>, } impl TomlPlatform { pub fn dev_dependencies(&self) -> Option<&BTreeMap> { self.dev_dependencies .as_ref() .or(self.dev_dependencies2.as_ref()) } pub fn build_dependencies(&self) -> Option<&BTreeMap> { self.build_dependencies .as_ref() .or(self.build_dependencies2.as_ref()) } } #[derive(Serialize, Debug, Clone)] pub struct InheritableLints { #[serde(skip_serializing_if = "is_false")] pub workspace: bool, #[serde(flatten)] pub lints: TomlLints, } impl InheritableLints { pub fn normalized(&self) -> Result<&TomlLints, UnresolvedError> { if self.workspace { Err(UnresolvedError) } else { Ok(&self.lints) } } } fn is_false(b: &bool) -> bool { !b } impl<'de> Deserialize<'de> for InheritableLints { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { struct InheritableLintsVisitor; impl<'de> de::Visitor<'de> for InheritableLintsVisitor { // The type that our Visitor is going to produce. type Value = InheritableLints; // Format a message stating what data this Visitor expects to receive. fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("a lints table") } // Deserialize MyMap from an abstract "map" provided by the // Deserializer. The MapAccess input is a callback provided by // the Deserializer to let us see each entry in the map. fn visit_map(self, mut access: M) -> Result where M: de::MapAccess<'de>, { let mut lints = TomlLints::new(); let mut workspace = false; // While there are entries remaining in the input, add them // into our map. while let Some(key) = access.next_key()? { if key == "workspace" { workspace = match access.next_value()? { Some(WorkspaceValue) => true, None => false, }; } else { let value = access.next_value()?; lints.insert(key, value); } } Ok(InheritableLints { workspace, lints }) } } deserializer.deserialize_map(InheritableLintsVisitor) } } pub type TomlLints = BTreeMap; pub type TomlToolLints = BTreeMap; #[derive(Serialize, Debug, Clone)] #[serde(untagged)] pub enum TomlLint { Level(TomlLintLevel), Config(TomlLintConfig), } impl<'de> Deserialize<'de> for TomlLint { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { UntaggedEnumVisitor::new() .string(|string| { TomlLintLevel::deserialize(string.into_deserializer()).map(TomlLint::Level) }) .map(|map| map.deserialize().map(TomlLint::Config)) .deserialize(deserializer) } } impl TomlLint { pub fn level(&self) -> TomlLintLevel { match self { Self::Level(level) => *level, Self::Config(config) => config.level, } } pub fn priority(&self) -> i8 { match self { Self::Level(_) => 0, Self::Config(config) => config.priority, } } pub fn config(&self) -> Option<&toml::Table> { match self { Self::Level(_) => None, Self::Config(config) => Some(&config.config), } } } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct TomlLintConfig { pub level: TomlLintLevel, #[serde(default)] pub priority: i8, #[serde(flatten)] pub config: toml::Table, } #[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum TomlLintLevel { Forbid, Deny, Warn, Allow, } #[derive(Copy, Clone, Debug)] pub struct InvalidCargoFeatures {} impl<'de> de::Deserialize<'de> for InvalidCargoFeatures { fn deserialize(_d: D) -> Result where D: de::Deserializer<'de>, { use serde::de::Error as _; Err(D::Error::custom( "the field `cargo-features` should be set at the top of Cargo.toml before any tables", )) } } /// A StringOrVec can be parsed from either a TOML string or array, /// but is always stored as a vector. #[derive(Clone, Debug, Serialize, Eq, PartialEq, PartialOrd, Ord)] pub struct StringOrVec(pub Vec); impl StringOrVec { pub fn iter<'a>(&'a self) -> std::slice::Iter<'a, String> { self.0.iter() } } impl<'de> de::Deserialize<'de> for StringOrVec { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { UntaggedEnumVisitor::new() .expecting("string or list of strings") .string(|value| Ok(StringOrVec(vec![value.to_owned()]))) .seq(|value| value.deserialize().map(StringOrVec)) .deserialize(deserializer) } } #[derive(Clone, Debug, Serialize, Eq, PartialEq)] #[serde(untagged)] pub enum StringOrBool { String(String), Bool(bool), } impl<'de> Deserialize<'de> for StringOrBool { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { UntaggedEnumVisitor::new() .bool(|b| Ok(StringOrBool::Bool(b))) .string(|s| Ok(StringOrBool::String(s.to_owned()))) .deserialize(deserializer) } } #[derive(PartialEq, Clone, Debug, Serialize)] #[serde(untagged)] pub enum VecStringOrBool { VecString(Vec), Bool(bool), } impl<'de> de::Deserialize<'de> for VecStringOrBool { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { UntaggedEnumVisitor::new() .expecting("a boolean or vector of strings") .bool(|value| Ok(VecStringOrBool::Bool(value))) .seq(|value| value.deserialize().map(VecStringOrBool::VecString)) .deserialize(deserializer) } } #[derive(Clone)] pub struct PathValue(pub PathBuf); impl fmt::Debug for PathValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } impl ser::Serialize for PathValue { fn serialize(&self, serializer: S) -> Result where S: ser::Serializer, { self.0.serialize(serializer) } } impl<'de> de::Deserialize<'de> for PathValue { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { Ok(PathValue(String::deserialize(deserializer)?.into())) } } /// Error validating names in Cargo. #[derive(Debug, thiserror::Error)] #[error("manifest field was not resolved")] #[non_exhaustive] pub struct UnresolvedError; cargo-util-schemas-0.7.0/src/manifest/rust_version.rs000064400000000000000000000153701046102023000210320ustar 00000000000000use std::fmt; use std::fmt::Display; use serde_untagged::UntaggedEnumVisitor; use crate::core::PartialVersion; use crate::core::PartialVersionError; #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize)] #[serde(transparent)] pub struct RustVersion(PartialVersion); impl RustVersion { pub fn is_compatible_with(&self, rustc: &PartialVersion) -> bool { let msrv = self.0.to_caret_req(); // Remove any pre-release identifiers for easier comparison let rustc = semver::Version { major: rustc.major, minor: rustc.minor.unwrap_or_default(), patch: rustc.patch.unwrap_or_default(), pre: Default::default(), build: Default::default(), }; msrv.matches(&rustc) } pub fn into_partial(self) -> PartialVersion { self.0 } pub fn as_partial(&self) -> &PartialVersion { &self.0 } } impl std::str::FromStr for RustVersion { type Err = RustVersionError; fn from_str(value: &str) -> Result { let partial = value.parse::(); let partial = partial.map_err(RustVersionErrorKind::PartialVersion)?; partial.try_into() } } impl TryFrom for RustVersion { type Error = RustVersionError; fn try_from(version: semver::Version) -> Result { let version = PartialVersion::from(version); Self::try_from(version) } } impl TryFrom for RustVersion { type Error = RustVersionError; fn try_from(partial: PartialVersion) -> Result { if partial.pre.is_some() { return Err(RustVersionErrorKind::Prerelease.into()); } if partial.build.is_some() { return Err(RustVersionErrorKind::BuildMetadata.into()); } Ok(Self(partial)) } } impl<'de> serde::Deserialize<'de> for RustVersion { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { UntaggedEnumVisitor::new() .expecting("SemVer version") .string(|value| value.parse().map_err(serde::de::Error::custom)) .deserialize(deserializer) } } impl Display for RustVersion { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } /// Error parsing a [`RustVersion`]. #[derive(Debug, thiserror::Error)] #[error(transparent)] pub struct RustVersionError(#[from] RustVersionErrorKind); /// Non-public error kind for [`RustVersionError`]. #[non_exhaustive] #[derive(Debug, thiserror::Error)] enum RustVersionErrorKind { #[error("unexpected prerelease field, expected a version like \"1.32\"")] Prerelease, #[error("unexpected build field, expected a version like \"1.32\"")] BuildMetadata, #[error(transparent)] PartialVersion(#[from] PartialVersionError), } #[cfg(test)] mod test { use super::*; use snapbox::prelude::*; use snapbox::str; #[test] fn is_compatible_with_rustc() { let cases = &[ ("1", "1.70.0", true), ("1.30", "1.70.0", true), ("1.30.10", "1.70.0", true), ("1.70", "1.70.0", true), ("1.70.0", "1.70.0", true), ("1.70.1", "1.70.0", false), ("1.70", "1.70.0-nightly", true), ("1.70.0", "1.70.0-nightly", true), ("1.71", "1.70.0", false), ("2", "1.70.0", false), ]; let mut passed = true; for (msrv, rustc, expected) in cases { let msrv: RustVersion = msrv.parse().unwrap(); let rustc = PartialVersion::from(semver::Version::parse(rustc).unwrap()); if msrv.is_compatible_with(&rustc) != *expected { println!("failed: {msrv} is_compatible_with {rustc} == {expected}"); passed = false; } } assert!(passed); } #[test] fn is_compatible_with_workspace_msrv() { let cases = &[ ("1", "1", true), ("1", "1.70", true), ("1", "1.70.0", true), ("1.30", "1", false), ("1.30", "1.70", true), ("1.30", "1.70.0", true), ("1.30.10", "1", false), ("1.30.10", "1.70", true), ("1.30.10", "1.70.0", true), ("1.70", "1", false), ("1.70", "1.70", true), ("1.70", "1.70.0", true), ("1.70.0", "1", false), ("1.70.0", "1.70", true), ("1.70.0", "1.70.0", true), ("1.70.1", "1", false), ("1.70.1", "1.70", false), ("1.70.1", "1.70.0", false), ("1.71", "1", false), ("1.71", "1.70", false), ("1.71", "1.70.0", false), ("2", "1.70.0", false), ]; let mut passed = true; for (dep_msrv, ws_msrv, expected) in cases { let dep_msrv: RustVersion = dep_msrv.parse().unwrap(); let ws_msrv = ws_msrv.parse::().unwrap().into_partial(); if dep_msrv.is_compatible_with(&ws_msrv) != *expected { println!("failed: {dep_msrv} is_compatible_with {ws_msrv} == {expected}"); passed = false; } } assert!(passed); } #[test] fn parse_errors() { let cases = &[ // Disallow caret ( "^1.43", str![[r#"unexpected version requirement, expected a version like "1.32""#]], ), // Valid pre-release ( "1.43.0-beta.1", str![[r#"unexpected prerelease field, expected a version like "1.32""#]], ), // Bad pre-release ( "1.43-beta.1", str![[r#"unexpected prerelease field, expected a version like "1.32""#]], ), // Weird wildcard ( "x", str![[r#"unexpected version requirement, expected a version like "1.32""#]], ), ( "1.x", str![[r#"unexpected version requirement, expected a version like "1.32""#]], ), ( "1.1.x", str![[r#"unexpected version requirement, expected a version like "1.32""#]], ), // Non-sense ("foodaddle", str![[r#"expected a version like "1.32""#]]), ]; for (input, expected) in cases { let actual: Result = input.parse(); let actual = match actual { Ok(result) => format!("didn't fail: {result:?}"), Err(err) => err.to_string(), }; snapbox::assert_data_eq!(actual, expected.clone().raw()); } } } cargo-util-schemas-0.7.0/src/restricted_names.rs000064400000000000000000000205661046102023000200200ustar 00000000000000//! Helpers for validating and checking names like package and crate names. type Result = std::result::Result; /// Error validating names in Cargo. #[derive(Debug, thiserror::Error)] #[error(transparent)] pub struct NameValidationError(#[from] ErrorKind); /// Non-public error kind for [`NameValidationError`]. #[non_exhaustive] #[derive(Debug, thiserror::Error)] enum ErrorKind { #[error("{0} cannot be empty")] Empty(&'static str), #[error("invalid character `{ch}` in {what}: `{name}`, {reason}")] InvalidCharacter { ch: char, what: &'static str, name: String, reason: &'static str, }, #[error( "profile name `{name}` is reserved\n{help}\n\ See https://doc.rust-lang.org/cargo/reference/profiles.html \ for more on configuring profiles." )] ProfileNameReservedKeyword { name: String, help: &'static str }, #[error("feature named `{0}` is not allowed to start with `dep:`")] FeatureNameStartsWithDepColon(String), } pub(crate) fn validate_package_name(name: &str) -> Result<()> { for part in name.split("::") { validate_name(part, "package name")?; } Ok(()) } pub(crate) fn validate_registry_name(name: &str) -> Result<()> { validate_name(name, "registry name") } pub(crate) fn validate_name(name: &str, what: &'static str) -> Result<()> { if name.is_empty() { return Err(ErrorKind::Empty(what).into()); } let mut chars = name.chars(); if let Some(ch) = chars.next() { if ch.is_digit(10) { // A specific error for a potentially common case. return Err(ErrorKind::InvalidCharacter { ch, what, name: name.into(), reason: "the name cannot start with a digit", } .into()); } if !(unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_') { return Err(ErrorKind::InvalidCharacter { ch, what, name: name.into(), reason: "the first character must be a Unicode XID start character \ (most letters or `_`)", } .into()); } } for ch in chars { if !(unicode_xid::UnicodeXID::is_xid_continue(ch) || ch == '-') { return Err(ErrorKind::InvalidCharacter { ch, what, name: name.into(), reason: "characters must be Unicode XID characters \ (numbers, `-`, `_`, or most letters)", } .into()); } } Ok(()) } /// Ensure a package name is [valid][validate_package_name] pub(crate) fn sanitize_package_name(name: &str, placeholder: char) -> String { let mut slug = String::new(); for part in name.split("::") { if !slug.is_empty() { slug.push_str("::"); } slug.push_str(&sanitize_name(part, placeholder)); } slug } pub(crate) fn sanitize_name(name: &str, placeholder: char) -> String { let mut slug = String::new(); let mut chars = name.chars(); while let Some(ch) = chars.next() { if (unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_') && !ch.is_digit(10) { slug.push(ch); break; } } while let Some(ch) = chars.next() { if unicode_xid::UnicodeXID::is_xid_continue(ch) || ch == '-' { slug.push(ch); } else { slug.push(placeholder); } } if slug.is_empty() { slug.push_str("package"); } slug } /// Validate dir-names and profile names according to RFC 2678. pub(crate) fn validate_profile_name(name: &str) -> Result<()> { if let Some(ch) = name .chars() .find(|ch| !ch.is_alphanumeric() && *ch != '_' && *ch != '-') { return Err(ErrorKind::InvalidCharacter { ch, what: "profile name", name: name.into(), reason: "allowed characters are letters, numbers, underscore, and hyphen", } .into()); } let lower_name = name.to_lowercase(); if lower_name == "debug" { return Err(ErrorKind::ProfileNameReservedKeyword { name: name.into(), help: "To configure the default development profile, \ use the name `dev` as in [profile.dev]", } .into()); } if lower_name == "build-override" { return Err(ErrorKind::ProfileNameReservedKeyword { name: name.into(), help: "To configure build dependency settings, use [profile.dev.build-override] \ and [profile.release.build-override]", } .into()); } // These are some arbitrary reservations. We have no plans to use // these, but it seems safer to reserve a few just in case we want to // add more built-in profiles in the future. We can also uses special // syntax like cargo:foo if needed. But it is unlikely these will ever // be used. if matches!( lower_name.as_str(), "build" | "check" | "clean" | "config" | "fetch" | "fix" | "install" | "metadata" | "package" | "publish" | "report" | "root" | "run" | "rust" | "rustc" | "rustdoc" | "target" | "tmp" | "uninstall" ) || lower_name.starts_with("cargo") { return Err(ErrorKind::ProfileNameReservedKeyword { name: name.into(), help: "Please choose a different name.", } .into()); } Ok(()) } pub(crate) fn validate_feature_name(name: &str) -> Result<()> { let what = "feature name"; if name.is_empty() { return Err(ErrorKind::Empty(what).into()); } if name.starts_with("dep:") { return Err(ErrorKind::FeatureNameStartsWithDepColon(name.into()).into()); } if name.contains('/') { return Err(ErrorKind::InvalidCharacter { ch: '/', what, name: name.into(), reason: "feature name is not allowed to contain slashes", } .into()); } let mut chars = name.chars(); if let Some(ch) = chars.next() { if !(unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_' || ch.is_digit(10)) { return Err(ErrorKind::InvalidCharacter { ch, what, name: name.into(), reason: "the first character must be a Unicode XID start character or digit \ (most letters or `_` or `0` to `9`)", } .into()); } } for ch in chars { if !(unicode_xid::UnicodeXID::is_xid_continue(ch) || ch == '-' || ch == '+' || ch == '.') { return Err(ErrorKind::InvalidCharacter { ch, what, name: name.into(), reason: "characters must be Unicode XID characters, '-', `+`, or `.` \ (numbers, `+`, `-`, `_`, `.`, or most letters)", } .into()); } } Ok(()) } pub(crate) fn validate_path_base_name(name: &str) -> Result<()> { validate_name(name, "path base name") } #[cfg(test)] mod tests { use super::*; #[test] fn valid_feature_names() { assert!(validate_feature_name("c++17").is_ok()); assert!(validate_feature_name("128bit").is_ok()); assert!(validate_feature_name("_foo").is_ok()); assert!(validate_feature_name("feat-name").is_ok()); assert!(validate_feature_name("feat_name").is_ok()); assert!(validate_feature_name("foo.bar").is_ok()); assert!(validate_feature_name("").is_err()); assert!(validate_feature_name("+foo").is_err()); assert!(validate_feature_name("-foo").is_err()); assert!(validate_feature_name(".foo").is_err()); assert!(validate_feature_name("dep:bar").is_err()); assert!(validate_feature_name("foo/bar").is_err()); assert!(validate_feature_name("foo:bar").is_err()); assert!(validate_feature_name("foo?").is_err()); assert!(validate_feature_name("?foo").is_err()); assert!(validate_feature_name("ⒶⒷⒸ").is_err()); assert!(validate_feature_name("a¼").is_err()); assert!(validate_feature_name("").is_err()); } }