Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve flake_ref parsing #12

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/runix/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ shell-escape = "0.1.5"
tokio = { version = "1.21", features = ["full"] }
tokio-stream = { version = "0.1.11", features = ["tokio-util", "io-util"] }
thiserror = "1.0"
url = "2.3"
url = { version = "2.3", features = [ "serde" ] }
193 changes: 149 additions & 44 deletions crates/runix/src/flake_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
//!
//! Parses flake reference Urls as defined by the Nix reference implementation.

use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap};
use std::num::ParseIntError;
use std::path::PathBuf;
use std::str::FromStr;
use std::path::{Path, PathBuf};
use std::str::{FromStr, ParseBoolError};

use log::info;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -35,11 +35,19 @@ pub enum FlakeRefError {
ParseLastModified(ParseIntError),
#[error("Could not parse `revCount` field as Integer")]
ParseRevCount(ParseIntError),

#[error("Could not parse `shallow` field: {0}")]
ParseShallow(ParseBoolError),
#[error("Could not parse `submodules` field: {0}")]
ParseSumbodules(ParseBoolError),
#[error("Could not parse `allRefs` field: {0}")]
ParseAllRefs(ParseBoolError),
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct IndirectFlake {
pub id: FlakeId,
pub args: BTreeMap<String, String>,
}

/// Flake ref definitions
Expand All @@ -65,20 +73,22 @@ pub enum ToFlakeRef {
},
/// https://cs.github.com/NixOS/nix/blob/f225f4307662fe9a57543d0c86c28aa9fddaf0d2/src/libfetchers/git.cc#L287
Git {
url: GitUrl,
url: Url,
shallow: Option<bool>,
submodules: Option<bool>,
#[serde(rename = "allRefs")]
all_refs: Option<bool>,

#[serde(rename = "ref")]
commit_ref: CommitRef,
commit_ref: Option<CommitRef>,

#[serde(rename = "revCount")]
rev_count: Option<RevCount>,

#[serde(flatten)]
pinned: Pinned,
pinned: Option<Pinned>,

dir: Option<PathBuf>,
},
/// https://cs.github.com/NixOS/nix/blob/f225f4307662fe9a57543d0c86c28aa9fddaf0d2/src/libfetchers/tarball.cc#L206
Tarball {
Expand Down Expand Up @@ -142,14 +152,25 @@ impl ToFlakeRef {
commit_ref: _,
rev_count: _,
pinned: _,
dir: _,
} => todo!(),
ToFlakeRef::Tarball {
url: _,
unpack: _,
nar_hash: _,
} => todo!(),
ToFlakeRef::Indirect(IndirectFlake { id }) => {
Url::parse(&format!("flake:{id}")).expect("Failed to create indirect reference")
ToFlakeRef::Indirect(IndirectFlake { id, args }) => {
let mut url = Url::parse(&format!("flake:{id}"))
.expect("Failed to create indirect reference");

// append query
{
let mut query = url.query_pairs_mut();
query.extend_pairs(args.iter());
query.finish();
}

url
},
};
Ok(url)
Expand All @@ -170,7 +191,58 @@ impl ToFlakeRef {
"github" => ToFlakeRef::GitHub(GitService::from_url(url)?),
"flake" => ToFlakeRef::Indirect(IndirectFlake {
id: url.path().to_string(),
args: url
.query_pairs()
.map(|(k, v)| (k.into_owned(), v.into_owned()))
.collect(),
}),
"git+http" | "git+https" | "git+ssh" | "git+file" => {
let pairs: HashMap<_, _> = url.query_pairs().collect();
let shallow = pairs
.get("shallow")
.map(|shallow| shallow.parse())
.transpose()
.map_err(FlakeRefError::ParseShallow)?;
let submodules = pairs
.get("submodules")
.map(|submodules| submodules.parse())
.transpose()
.map_err(FlakeRefError::ParseSumbodules)?;
let all_refs = pairs
.get("all_refs")
.map(|all_refs| all_refs.parse())
.transpose()
.map_err(FlakeRefError::ParseAllRefs)?;

let commit_ref = pairs.get("ref").map(|commit_ref| commit_ref.to_string());
let rev_count = pairs
.get("revCount")
.map(|rev_count| rev_count.parse())
.transpose()
.map_err(FlakeRefError::ParseRevCount)?;
let pinned = Pinned::from_query(url)?;
let dir = pairs
.get("dir")
.map(|dir| Path::new(dir.as_ref()).to_path_buf());

let wrapped_url = {
let mut url = url.clone();
url.set_query(None);
url.set_fragment(None);
Url::parse(url.to_string().trim_start_matches("git+")).unwrap()
};
Comment on lines +228 to +233
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks weird but is a result of the url lib being a bit too correct..


ToFlakeRef::Git {
url: wrapped_url,
shallow,
submodules,
all_refs,
commit_ref,
rev_count,
pinned,
dir,
}
},
_ => todo!(),
};
Ok(flake_ref)
Expand All @@ -182,9 +254,15 @@ impl FromStr for ToFlakeRef {

fn from_str(s: &str) -> Result<Self, Self::Err> {
let url = Url::parse(s).or_else(|e| {
info!("could not parse '{s}' as qualified url, trying to parse as `path:` ({e})",);
Url::parse(&format!("path:{}", s))
.map_err(|e| FlakeRefError::ParseUrl(s.to_string(), e))
if s.starts_with('.') || s.starts_with('/') {
info!("could not parse '{s}' as qualified url, trying to parse as `path:` ({e})",);
Url::parse(&format!("path://{s}"))
.map_err(|e| FlakeRefError::ParseUrl(s.to_string(), e))
} else {
info!("could not parse '{s}' as qualified url, trying to parse as `flake:` ({e})",);
Url::parse(&format!("flake:{s}"))
.map_err(|e| FlakeRefError::ParseUrl(s.to_string(), e))
}
})?;

ToFlakeRef::from_url(&url)
Expand Down Expand Up @@ -254,40 +332,29 @@ impl Pinned {
}

fn from_query(url: &Url) -> Result<Option<Self>, FlakeRefError> {
let nar_hash = url
.query_pairs()
.find(|(name, _)| name == "narHash")
.map(|(_, value)| value);
let last_modified = url
.query_pairs()
.find(|(name, _)| name == "lastModified")
.map(|(_, value)| value);
let rev = url
.query_pairs()
.find(|(name, _)| name == "rev")
.map(|(_, value)| value);

fn parse_last_modified(modified: Option<Cow<str>>) -> u64 {
modified
.map(|s| s.parse().unwrap_or_default())
.unwrap_or_default()
}

let pinned = match (nar_hash, rev, last_modified) {
(None, None, _) => None,
(None, Some(rev), modified) => Some(Self::Rev {
commit_rev: rev.into_owned(),
last_modified: parse_last_modified(modified),
let query: HashMap<_, _> = url.query_pairs().collect();

let nar_hash = query.get("narHash");
let rev = query.get("rev");
let last_modified = query
.get("lastModified")
.and_then(|s| s.parse().ok())
.unwrap_or_default();

let pinned = match (nar_hash, rev) {
(None, None) => None,
(None, Some(rev)) => Some(Self::Rev {
commit_rev: rev.to_string(),
last_modified,
}),
(Some(nar), None, modified) => Some(Self::Nar {
nar_hash: nar.into_owned(),
last_modified: parse_last_modified(modified),
(Some(nar), None) => Some(Self::Nar {
nar_hash: nar.to_string(),
last_modified,
}),
(Some(nar), Some(rev), modified) => Some(Self::NarAndRev {
nar_hash: nar.into_owned(),
last_modified: parse_last_modified(modified),

commit_rev: rev.into_owned(),
(Some(nar), Some(rev)) => Some(Self::NarAndRev {
nar_hash: nar.to_string(),
last_modified,
commit_rev: rev.to_string(),
}),
};

Expand All @@ -308,6 +375,8 @@ pub struct GitService {
commit_ref: Option<CommitRef>,
#[serde(flatten)]
pinned: Option<Pinned>,

dir: Option<PathBuf>,
}

impl GitService {
Expand Down Expand Up @@ -393,6 +462,39 @@ mod tests {
);
}

#[test]
fn parses_registry_flakeref() {
let expected = ToFlakeRef::Indirect(IndirectFlake {
id: "nixpkgs".to_string(),
args: BTreeMap::default(),
});

assert_eq!(&ToFlakeRef::from_str("flake:nixpkgs").unwrap(), &expected);
assert_eq!(&ToFlakeRef::from_str("nixpkgs").unwrap(), &expected);
}

#[test]
fn parses_git_path_flakeref() {
let expected = ToFlakeRef::Git {
url: Url::from_directory_path("/somewhere/on/the/drive").unwrap(),
shallow: Some(false),
submodules: Some(false),
all_refs: None,
commit_ref: Some("feature/xyz".to_owned()),
rev_count: None,
pinned: None,
dir: Some("abc".into()),
};

assert_eq!(
&ToFlakeRef::from_str(
"git+file:///somewhere/on/the/drive/?shallow=false&submodules=false&ref=feature/xyz&dir=abc"
)
.unwrap(),
&expected
);
}

#[test]
fn parses_github_flakeref() {
let flakeref = serde_json::from_str::<ToFlakeRef>(
Expand All @@ -414,7 +516,8 @@ mod tests {
repo: "nixpkgs".into(),
host: None,
commit_ref: Some("unstable".into()),
pinned: None
pinned: None,
dir: None,
})
)
}
Expand Down Expand Up @@ -488,6 +591,7 @@ mod tests {
host: None,
commit_ref: Some("unstable".into()),
pinned: None,
dir: None,
});

let parsed = ToFlakeRef::from_url(&flake_ref.to_url().expect("should serialize to url"))
Expand All @@ -501,6 +605,7 @@ mod tests {
fn indirect_to_from_url() {
let flake_ref = ToFlakeRef::Indirect(IndirectFlake {
id: "nixpkgs-flox".into(),
args: BTreeMap::default(),
});

let parsed = ToFlakeRef::from_url(&flake_ref.to_url().expect("should serialize to url"))
Expand Down
1 change: 1 addition & 0 deletions crates/runix/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ impl Registry {
let entry = RegistryEntry {
from: FromFlakeRef::Indirect(IndirectFlake {
id: name.to_string(),
args: Default::default(),
}),
to,
exact: None,
Expand Down