Skip to content

Commit c15a379

Browse files
committed
feat(schemas): SourceKind::Patched (unstable)
`SourceKind::Patched` represents a source patched by local patch files.
1 parent e332930 commit c15a379

File tree

4 files changed

+157
-4
lines changed

4 files changed

+157
-4
lines changed

Diff for: crates/cargo-util-schemas/src/core/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ pub use package_id_spec::PackageIdSpecError;
77
pub use partial_version::PartialVersion;
88
pub use partial_version::PartialVersionError;
99
pub use source_kind::GitReference;
10+
pub use source_kind::PatchInfo;
11+
pub use source_kind::PatchInfoError;
1012
pub use source_kind::SourceKind;

Diff for: crates/cargo-util-schemas/src/core/package_id_spec.rs

+46-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use url::Url;
77
use crate::core::GitReference;
88
use crate::core::PartialVersion;
99
use crate::core::PartialVersionError;
10+
use crate::core::PatchInfo;
1011
use crate::core::SourceKind;
1112
use crate::manifest::PackageName;
1213
use crate::restricted_names::NameValidationError;
@@ -145,6 +146,14 @@ impl PackageIdSpec {
145146
kind = Some(SourceKind::Path);
146147
url = strip_url_protocol(&url);
147148
}
149+
"patched" => {
150+
let patch_info =
151+
PatchInfo::from_query(url.query_pairs()).map_err(ErrorKind::PatchInfo)?;
152+
url.set_query(None);
153+
kind = Some(SourceKind::Patched(patch_info));
154+
// We don't strip protocol and leave `patch` as part of URL
155+
// in order to distinguish them.
156+
}
148157
kind => return Err(ErrorKind::UnsupportedProtocol(kind.into()).into()),
149158
}
150159
} else {
@@ -232,10 +241,16 @@ impl fmt::Display for PackageIdSpec {
232241
write!(f, "{protocol}+")?;
233242
}
234243
write!(f, "{}", url)?;
235-
if let Some(SourceKind::Git(git_ref)) = self.kind.as_ref() {
236-
if let Some(pretty) = git_ref.pretty_ref(true) {
237-
write!(f, "?{}", pretty)?;
244+
match self.kind.as_ref() {
245+
Some(SourceKind::Git(git_ref)) => {
246+
if let Some(pretty) = git_ref.pretty_ref(true) {
247+
write!(f, "?{pretty}")?;
248+
}
238249
}
250+
Some(SourceKind::Patched(patch_info)) => {
251+
write!(f, "?{}", patch_info.as_query())?;
252+
}
253+
_ => {}
239254
}
240255
if url.path_segments().unwrap().next_back().unwrap() != &*self.name {
241256
printed_name = true;
@@ -314,13 +329,16 @@ enum ErrorKind {
314329

315330
#[error(transparent)]
316331
PartialVersion(#[from] crate::core::PartialVersionError),
332+
333+
#[error(transparent)]
334+
PatchInfo(#[from] crate::core::PatchInfoError),
317335
}
318336

319337
#[cfg(test)]
320338
mod tests {
321339
use super::ErrorKind;
322340
use super::PackageIdSpec;
323-
use crate::core::{GitReference, SourceKind};
341+
use crate::core::{GitReference, PatchInfo, SourceKind};
324342
use url::Url;
325343

326344
#[test]
@@ -599,6 +617,18 @@ mod tests {
599617
},
600618
"path+file:///path/to/my/project/foo#1.1.8",
601619
);
620+
621+
// Unstable
622+
ok(
623+
"patched+https://crates.io/foo?name=bar&version=1.2.0&patch=%2Fto%2Fa.patch&patch=%2Fb.patch#[email protected]",
624+
PackageIdSpec {
625+
name: String::from("bar"),
626+
version: Some("1.2.0".parse().unwrap()),
627+
url: Some(Url::parse("patched+https://crates.io/foo").unwrap()),
628+
kind: Some(SourceKind::Patched(PatchInfo::new("bar".into(), "1.2.0".into(), vec!["/to/a.patch".into(), "/b.patch".into()]))),
629+
},
630+
"patched+https://crates.io/foo?name=bar&version=1.2.0&patch=%2Fto%2Fa.patch&patch=%2Fb.patch#[email protected]",
631+
);
602632
}
603633

604634
#[test]
@@ -651,5 +681,17 @@ mod tests {
651681
err!("@1.2.3", ErrorKind::NameValidation(_));
652682
err!("registry+https://github.com", ErrorKind::NameValidation(_));
653683
err!("https://crates.io/1foo#1.2.3", ErrorKind::NameValidation(_));
684+
err!(
685+
"patched+https://crates.io/foo?version=1.2.0&patch=%2Fb.patch#[email protected]",
686+
ErrorKind::PatchInfo(_)
687+
);
688+
err!(
689+
"patched+https://crates.io/foo?name=bar&patch=%2Fb.patch#[email protected]",
690+
ErrorKind::PatchInfo(_)
691+
);
692+
err!(
693+
"patched+https://crates.io/foo?name=bar&version=1.2.0&#[email protected]",
694+
ErrorKind::PatchInfo(_)
695+
);
654696
}
655697
}

Diff for: crates/cargo-util-schemas/src/core/source_kind.rs

+107
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::cmp::Ordering;
2+
use std::path::PathBuf;
23

34
/// The possible kinds of code source.
45
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -15,6 +16,8 @@ pub enum SourceKind {
1516
LocalRegistry,
1617
/// A directory-based registry.
1718
Directory,
19+
/// A source with paths to patch files (unstable).
20+
Patched(PatchInfo),
1821
}
1922

2023
impl SourceKind {
@@ -27,6 +30,8 @@ impl SourceKind {
2730
SourceKind::SparseRegistry => None,
2831
SourceKind::LocalRegistry => Some("local-registry"),
2932
SourceKind::Directory => Some("directory"),
33+
// Patched source URL already includes the `patched+` prefix, see `SourceId::new`
34+
SourceKind::Patched(_) => None,
3035
}
3136
}
3237
}
@@ -107,6 +112,10 @@ impl Ord for SourceKind {
107112
(SourceKind::Directory, _) => Ordering::Less,
108113
(_, SourceKind::Directory) => Ordering::Greater,
109114

115+
(SourceKind::Patched(a), SourceKind::Patched(b)) => a.cmp(b),
116+
(SourceKind::Patched(_), _) => Ordering::Less,
117+
(_, SourceKind::Patched(_)) => Ordering::Greater,
118+
110119
(SourceKind::Git(a), SourceKind::Git(b)) => a.cmp(b),
111120
}
112121
}
@@ -199,3 +208,101 @@ impl<'a> std::fmt::Display for PrettyRef<'a> {
199208
Ok(())
200209
}
201210
}
211+
212+
/// Information to find the source package and patch files.
213+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
214+
pub struct PatchInfo {
215+
/// Name of the package to be patched.
216+
name: String,
217+
/// Verision of the package to be patched.
218+
version: String,
219+
/// Absolute paths to patch files.
220+
///
221+
/// These are absolute to ensure Cargo can locate them in the patching phase.
222+
patches: Vec<PathBuf>,
223+
}
224+
225+
impl PatchInfo {
226+
pub fn new(name: String, version: String, patches: Vec<PathBuf>) -> PatchInfo {
227+
PatchInfo {
228+
name,
229+
version,
230+
patches,
231+
}
232+
}
233+
234+
/// Collects patch information from query string.
235+
///
236+
/// * `name` --- Package name
237+
/// * `version` --- Package exact version
238+
/// * `patch` --- Paths to patch files. Mutiple occurrences allowed.
239+
pub fn from_query(
240+
query_pairs: impl Iterator<Item = (impl AsRef<str>, impl AsRef<str>)>,
241+
) -> Result<PatchInfo, PatchInfoError> {
242+
let mut name = None;
243+
let mut version = None;
244+
let mut patches = Vec::new();
245+
for (k, v) in query_pairs {
246+
let v = v.as_ref();
247+
match k.as_ref() {
248+
"name" => name = Some(v.to_owned()),
249+
"version" => version = Some(v.to_owned()),
250+
"patch" => patches.push(PathBuf::from(v)),
251+
_ => {}
252+
}
253+
}
254+
let name = name.ok_or_else(|| PatchInfoError("name"))?;
255+
let version = version.ok_or_else(|| PatchInfoError("version"))?;
256+
if patches.is_empty() {
257+
return Err(PatchInfoError("path"));
258+
}
259+
Ok(PatchInfo::new(name, version, patches))
260+
}
261+
262+
/// As a URL query string.
263+
pub fn as_query(&self) -> PatchInfoQuery<'_> {
264+
PatchInfoQuery(self)
265+
}
266+
267+
pub fn name(&self) -> &str {
268+
self.name.as_str()
269+
}
270+
271+
pub fn version(&self) -> &str {
272+
self.version.as_str()
273+
}
274+
275+
pub fn patches(&self) -> &[PathBuf] {
276+
self.patches.as_slice()
277+
}
278+
}
279+
280+
/// A [`PatchInfo`] that can be `Display`ed as URL query string.
281+
pub struct PatchInfoQuery<'a>(&'a PatchInfo);
282+
283+
impl<'a> std::fmt::Display for PatchInfoQuery<'a> {
284+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285+
write!(f, "name=")?;
286+
for value in url::form_urlencoded::byte_serialize(self.0.name.as_bytes()) {
287+
write!(f, "{value}")?;
288+
}
289+
write!(f, "&version=")?;
290+
for value in url::form_urlencoded::byte_serialize(self.0.version.as_bytes()) {
291+
write!(f, "{value}")?;
292+
}
293+
for path in &self.0.patches {
294+
write!(f, "&patch=")?;
295+
let path = path.to_str().expect("utf8 patch").replace("\\", "/");
296+
for value in url::form_urlencoded::byte_serialize(path.as_bytes()) {
297+
write!(f, "{value}")?;
298+
}
299+
}
300+
301+
Ok(())
302+
}
303+
}
304+
305+
/// Error parsing patch info from URL query string.
306+
#[derive(Debug, thiserror::Error)]
307+
#[error("missing query string `{0}`")]
308+
pub struct PatchInfoError(&'static str);

Diff for: src/cargo/core/source_id.rs

+2
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ impl SourceId {
419419
.expect("path sources cannot be remote");
420420
Ok(Box::new(DirectorySource::new(&path, self, gctx)))
421421
}
422+
SourceKind::Patched(_) => todo!(),
422423
}
423424
}
424425

@@ -665,6 +666,7 @@ impl fmt::Display for SourceId {
665666
}
666667
SourceKind::LocalRegistry => write!(f, "registry `{}`", url_display(&self.inner.url)),
667668
SourceKind::Directory => write!(f, "dir {}", url_display(&self.inner.url)),
669+
SourceKind::Patched(_) => todo!(),
668670
}
669671
}
670672
}

0 commit comments

Comments
 (0)