use crate::value::*;
pub(crate) fn parse_flake_ref(s: &str) -> Result<Value, EvalError> {
fn split_query(s: &str) -> (&str, Vec<(String, String)>) {
match s.split_once('?') {
None => (s, Vec::new()),
Some((base, q)) => {
let params: Vec<(String, String)> = q
.split('&')
.filter(|p| !p.is_empty())
.map(|p| match p.split_once('=') {
Some((k, v)) => (k.to_string(), percent_decode(v)),
None => (p.to_string(), String::new()),
})
.collect();
(base, params)
}
}
}
fn percent_decode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len()
&& let Ok(b) = u8::from_str_radix(
std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or("00"),
16,
) {
out.push(b as char);
i += 3;
continue;
}
out.push(bytes[i] as char);
i += 1;
}
out
}
let mut attrs = NixAttrs::new();
for (scheme, ty) in &[
("github:", "github"),
("gitlab:", "gitlab"),
("sourcehut:", "sourcehut"),
] {
if let Some(rest) = s.strip_prefix(*scheme) {
let (base, params) = split_query(rest);
let parts: Vec<&str> = base.splitn(3, '/').collect();
if parts.len() < 2 {
return Err(EvalError::TypeError(format!(
"parseFlakeRef: '{s}' missing owner/repo"
)));
}
attrs.insert("type".into(), Value::string(*ty));
attrs.insert("owner".into(), Value::string(parts[0].to_string()));
attrs.insert("repo".into(), Value::string(parts[1].to_string()));
if let Some(reff) = parts.get(2)
&& !reff.is_empty() {
attrs.insert("ref".into(), Value::string((*reff).to_string()));
}
for (k, v) in params {
attrs.insert(k, Value::string(v));
}
return Ok(Value::Attrs(Rc::new(attrs)));
}
}
if let Some(rest) = s.strip_prefix("git+") {
let (base, params) = split_query(rest);
attrs.insert("type".into(), Value::string("git"));
attrs.insert("url".into(), Value::string(base.to_string()));
for (k, v) in params {
attrs.insert(k, Value::string(v));
}
return Ok(Value::Attrs(Rc::new(attrs)));
}
if let Some(rest) = s.strip_prefix("tarball+") {
let (base, params) = split_query(rest);
attrs.insert("type".into(), Value::string("tarball"));
attrs.insert("url".into(), Value::string(base.to_string()));
for (k, v) in params {
attrs.insert(k, Value::string(v));
}
return Ok(Value::Attrs(Rc::new(attrs)));
}
if let Some(p) = s.strip_prefix("path:") {
attrs.insert("type".into(), Value::string("path"));
attrs.insert("path".into(), Value::string(p.to_string()));
return Ok(Value::Attrs(Rc::new(attrs)));
}
if s.starts_with('/') {
attrs.insert("type".into(), Value::string("path"));
attrs.insert("path".into(), Value::string(s.to_string()));
return Ok(Value::Attrs(Rc::new(attrs)));
}
{
let bare = s.strip_prefix("flake:").unwrap_or(s);
let first_segment = bare.split('/').next().unwrap_or("");
let looks_indirect = !first_segment.is_empty()
&& !first_segment.contains(':')
&& !first_segment.contains('.')
&& first_segment
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
if looks_indirect {
let (base, params) = split_query(bare);
let parts: Vec<&str> = base.splitn(3, '/').collect();
attrs.insert("type".into(), Value::string("indirect"));
attrs.insert("id".into(), Value::string(parts[0].to_string()));
if let Some(reff) = parts.get(1)
&& !reff.is_empty()
{
attrs.insert("ref".into(), Value::string((*reff).to_string()));
}
if let Some(rev) = parts.get(2)
&& !rev.is_empty()
{
attrs.insert("rev".into(), Value::string((*rev).to_string()));
}
for (k, v) in params {
attrs.insert(k, Value::string(v));
}
return Ok(Value::Attrs(Rc::new(attrs)));
}
}
Err(EvalError::TypeError(format!(
"parseFlakeRef: '{s}' is not a recognised flake reference"
)))
}
pub(crate) fn flake_ref_to_string(attrs: &NixAttrs) -> Result<Value, EvalError> {
let ty = attrs
.get("type")
.ok_or_else(|| EvalError::AttrNotFound("type".into()))?
.to_str()?;
fn query_string(attrs: &NixAttrs, exclude: &[&str]) -> Result<String, EvalError> {
let mut params: Vec<(String, String)> = Vec::new();
for (k, v) in attrs.iter() {
if exclude.contains(&k.as_str()) {
continue;
}
params.push((k.clone(), v.to_str()?));
}
params.sort_by(|a, b| a.0.cmp(&b.0));
if params.is_empty() {
return Ok(String::new());
}
let parts: Vec<String> = params
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect();
Ok(format!("?{}", parts.join("&")))
}
match ty.as_str() {
"github" | "gitlab" | "sourcehut" => {
let owner = attrs
.get("owner")
.ok_or_else(|| EvalError::AttrNotFound("owner".into()))?
.to_str()?;
let repo = attrs
.get("repo")
.ok_or_else(|| EvalError::AttrNotFound("repo".into()))?
.to_str()?;
let mut out = format!("{ty}:{owner}/{repo}");
if let Some(rev) = attrs.get("rev") {
out.push('/');
out.push_str(&rev.to_str()?);
} else if let Some(reff) = attrs.get("ref") {
out.push('/');
out.push_str(&reff.to_str()?);
}
out.push_str(&query_string(
attrs,
&["type", "owner", "repo", "ref", "rev"],
)?);
Ok(Value::string(out))
}
"git" => {
let url = attrs
.get("url")
.ok_or_else(|| EvalError::AttrNotFound("url".into()))?
.to_str()?;
let qs = query_string(attrs, &["type", "url"])?;
Ok(Value::string(format!("git+{url}{qs}")))
}
"tarball" => {
let url = attrs
.get("url")
.ok_or_else(|| EvalError::AttrNotFound("url".into()))?
.to_str()?;
let qs = query_string(attrs, &["type", "url"])?;
if (url.starts_with("http://") || url.starts_with("https://")) && qs.is_empty() {
Ok(Value::string(url))
} else {
Ok(Value::string(format!("tarball+{url}{qs}")))
}
}
"path" => {
let path = attrs
.get("path")
.ok_or_else(|| EvalError::AttrNotFound("path".into()))?
.to_str()?;
let qs = query_string(attrs, &["type", "path"])?;
Ok(Value::string(format!("path:{path}{qs}")))
}
"indirect" => {
let id = attrs
.get("id")
.ok_or_else(|| EvalError::AttrNotFound("id".into()))?
.to_str()?;
let mut out = format!("flake:{id}");
if let Some(rev) = attrs.get("rev") {
out.push('/');
out.push_str(&rev.to_str()?);
} else if let Some(reff) = attrs.get("ref") {
out.push('/');
out.push_str(&reff.to_str()?);
}
out.push_str(&query_string(attrs, &["type", "id", "ref", "rev"])?);
Ok(Value::string(out))
}
other => Err(EvalError::TypeError(format!(
"flakeRefToString: unknown flake type '{other}'"
))),
}
}