1use std::fmt::Display;
2
3use serde::{Deserialize, Serialize};
4use winnow::{
5 ModalResult, Parser,
6 combinator::{repeat, separated_pair},
7 error::{StrContext, StrContextValue},
8 token::{take_till, take_until},
9};
10
11use crate::{
12 error::NixUriError,
13 flakeref::{encoding, validators::parse_bool_param},
14};
15
16#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
25#[cfg_attr(test, serde(deny_unknown_fields))]
26#[non_exhaustive]
27pub struct LocationParameters {
28 dir: Option<String>,
32 #[serde(rename = "narHash")]
36 nar_hash: Option<String>,
37 pub submodules: Option<bool>,
41 pub shallow: Option<bool>,
45 host: Option<String>,
47 #[serde(rename = "revCount")]
49 rev_count: Option<String>,
50 #[serde(rename = "lastModified")]
52 last_modified: Option<String>,
53 pub lfs: Option<bool>,
58 #[serde(rename = "exportIgnore")]
60 pub export_ignore: Option<bool>,
61 #[serde(rename = "allRefs")]
63 pub all_refs: Option<bool>,
64 #[serde(rename = "verifyCommit")]
66 pub verify_commit: Option<bool>,
67 pub keytype: Option<String>,
70 #[serde(rename = "publicKey")]
72 pub public_key: Option<String>,
73 #[serde(rename = "publicKeys")]
76 pub public_keys: Option<String>,
77 arbitrary: Vec<(String, String)>,
82}
83
84#[derive(Debug, Default, Clone, PartialEq, Eq)]
89pub(crate) struct ParamRefRev {
90 pub r#ref: Option<String>,
91 pub rev: Option<String>,
92}
93
94impl Display for LocationParameters {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 let mut entries = self.entries();
97 entries.sort_by(|a, b| a.0.cmp(b.0));
98 for (i, (key, value)) in entries.iter().enumerate() {
99 if i > 0 {
100 write!(f, "&")?;
101 }
102 write!(
103 f,
104 "{key}={value}",
105 key = encoding::encode_query(key),
106 value = encoding::encode_query(value)
107 )?;
108 }
109 Ok(())
110 }
111}
112
113impl LocationParameters {
114 #[allow(dead_code)]
115 pub(crate) fn parse(input: &mut &str) -> ModalResult<(Self, ParamRefRev)> {
116 let param_values: Vec<(&str, &str)> = repeat(
117 0..,
118 separated_pair(
119 take_until(0.., "="),
120 '='.context(StrContext::Expected(StrContextValue::CharLiteral('='))),
121 take_till(0.., |c| c == '&' || c == '#'),
122 ),
123 )
124 .context(StrContext::Label("location parameters"))
125 .parse_next(input)?;
126
127 let mut params = Self::default();
128 let mut ref_rev = ParamRefRev::default();
129 for (param, value) in param_values {
130 if let Ok(param) = param.parse() {
134 match param {
135 LocationParamKeys::Dir => params.set_dir(Some(value.into())),
136 LocationParamKeys::NarHash => params.set_nar_hash(Some(value.into())),
137 LocationParamKeys::LastModified => {
138 params.set_last_modified(Some(value.into()));
139 }
140 LocationParamKeys::RevCount => params.set_rev_count(Some(value.into())),
141 LocationParamKeys::Host => params.set_host(Some(value.into())),
142 LocationParamKeys::Ref => ref_rev.r#ref = Some(value.into()),
143 LocationParamKeys::Rev => ref_rev.rev = Some(value.into()),
144 LocationParamKeys::Submodules => {
145 params.set_submodules(parse_bool_param("submodules", value).ok());
146 }
147 LocationParamKeys::Shallow => {
148 params.set_shallow(parse_bool_param("shallow", value).ok());
149 }
150 LocationParamKeys::Lfs => {
151 params.set_lfs(parse_bool_param("lfs", value).ok());
152 }
153 LocationParamKeys::ExportIgnore => {
154 params.set_export_ignore(parse_bool_param("exportIgnore", value).ok());
155 }
156 LocationParamKeys::AllRefs => {
157 params.set_all_refs(parse_bool_param("allRefs", value).ok());
158 }
159 LocationParamKeys::VerifyCommit => {
160 params.set_verify_commit(parse_bool_param("verifyCommit", value).ok());
161 }
162 LocationParamKeys::Keytype => params.set_keytype(Some(value.into())),
163 LocationParamKeys::PublicKey => params.set_public_key(Some(value.into())),
164 LocationParamKeys::PublicKeys => params.set_public_keys(Some(value.into())),
165 LocationParamKeys::Arbitrary(param) => {
166 params.add_arbitrary((param, value.into()));
167 }
168 }
169 }
170 }
171 Ok((params, ref_rev))
172 }
173
174 pub fn dir(&mut self, dir: Option<String>) -> &mut Self {
177 self.dir = dir;
178 self
179 }
180
181 pub fn nar_hash(&mut self, nar_hash: Option<String>) -> &mut Self {
183 self.nar_hash = nar_hash;
184 self
185 }
186
187 pub fn host(&mut self, host: Option<String>) -> &mut Self {
189 self.host = host;
190 self
191 }
192
193 pub fn set_dir(&mut self, dir: Option<String>) {
195 self.dir = dir;
196 }
197
198 pub fn set_nar_hash(&mut self, nar_hash: Option<String>) {
200 self.nar_hash = nar_hash;
201 }
202
203 pub fn set_host(&mut self, host: Option<String>) {
206 self.host = host;
207 }
208
209 pub(crate) fn host_value(&self) -> Option<&str> {
213 self.host.as_deref()
214 }
215
216 pub(crate) fn nar_hash_value(&self) -> Option<&str> {
221 self.nar_hash.as_deref()
222 }
223
224 pub(crate) fn submodules_truthy(&self) -> bool {
229 self.submodules.unwrap_or(false)
230 }
231
232 pub(crate) fn shallow_truthy(&self) -> bool {
235 self.shallow.unwrap_or(false)
236 }
237
238 pub fn rev_count_mut(&mut self) -> &mut Option<String> {
240 &mut self.rev_count
241 }
242
243 pub fn set_last_modified(&mut self, last_modified: Option<String>) {
245 self.last_modified = last_modified;
246 }
247
248 pub fn set_rev_count(&mut self, rev_count: Option<String>) {
250 self.rev_count = rev_count;
251 }
252
253 pub fn set_submodules(&mut self, submodules: Option<bool>) {
255 self.submodules = submodules;
256 }
257
258 pub fn set_shallow(&mut self, shallow: Option<bool>) {
260 self.shallow = shallow;
261 }
262
263 pub fn set_lfs(&mut self, lfs: Option<bool>) {
265 self.lfs = lfs;
266 }
267
268 pub fn set_export_ignore(&mut self, export_ignore: Option<bool>) {
270 self.export_ignore = export_ignore;
271 }
272
273 pub fn set_all_refs(&mut self, all_refs: Option<bool>) {
275 self.all_refs = all_refs;
276 }
277
278 pub fn set_verify_commit(&mut self, verify_commit: Option<bool>) {
280 self.verify_commit = verify_commit;
281 }
282
283 pub fn set_keytype(&mut self, keytype: Option<String>) {
285 self.keytype = keytype;
286 }
287
288 pub fn set_public_key(&mut self, public_key: Option<String>) {
290 self.public_key = public_key;
291 }
292
293 pub fn set_public_keys(&mut self, public_keys: Option<String>) {
295 self.public_keys = public_keys;
296 }
297
298 pub fn add_arbitrary(&mut self, arbitrary: (String, String)) {
302 self.arbitrary.push(arbitrary);
303 }
304
305 pub(crate) fn entries(&self) -> Vec<(&str, &str)> {
311 let mut entries: Vec<(&str, &str)> = Vec::new();
312 if let Some(v) = &self.dir {
313 entries.push(("dir", v));
314 }
315 if let Some(v) = &self.host {
316 entries.push(("host", v));
317 }
318 if let Some(v) = &self.nar_hash {
319 entries.push(("narHash", v));
320 }
321 if let Some(v) = &self.last_modified {
322 entries.push(("lastModified", v));
323 }
324 if let Some(v) = &self.rev_count {
325 entries.push(("revCount", v));
326 }
327 if let Some(v) = self.submodules {
328 entries.push(("submodules", bool_repr(v)));
329 }
330 if let Some(v) = self.shallow {
331 entries.push(("shallow", bool_repr(v)));
332 }
333 if let Some(v) = self.lfs {
334 entries.push(("lfs", bool_repr(v)));
335 }
336 if let Some(v) = self.export_ignore {
337 entries.push(("exportIgnore", bool_repr(v)));
338 }
339 if let Some(v) = self.all_refs {
340 entries.push(("allRefs", bool_repr(v)));
341 }
342 if let Some(v) = self.verify_commit {
343 entries.push(("verifyCommit", bool_repr(v)));
344 }
345 if let Some(v) = &self.keytype {
346 entries.push(("keytype", v));
347 }
348 if let Some(v) = &self.public_key {
349 entries.push(("publicKey", v));
350 }
351 if let Some(v) = &self.public_keys {
352 entries.push(("publicKeys", v));
353 }
354 for (k, v) in &self.arbitrary {
355 entries.push((k.as_str(), v.as_str()));
356 }
357 entries
358 }
359}
360
361fn bool_repr(b: bool) -> &'static str {
366 if b { "1" } else { "0" }
367}
368
369#[non_exhaustive]
370pub(crate) enum LocationParamKeys {
371 Dir,
372 NarHash,
373 LastModified,
374 RevCount,
375 Host,
376 Ref,
377 Rev,
378 Submodules,
379 Shallow,
380 Lfs,
381 ExportIgnore,
382 AllRefs,
383 VerifyCommit,
384 Keytype,
385 PublicKey,
386 PublicKeys,
387 Arbitrary(String),
388}
389
390impl std::str::FromStr for LocationParamKeys {
391 type Err = NixUriError;
392
393 fn from_str(s: &str) -> Result<Self, Self::Err> {
394 match s {
395 "dir" | "&dir" => Ok(Self::Dir),
396 "narHash" | "&narHash" => Ok(Self::NarHash),
397 "lastModified" | "&lastModified" => Ok(Self::LastModified),
398 "revCount" | "&revCount" => Ok(Self::RevCount),
399 "host" | "&host" => Ok(Self::Host),
400 "rev" | "&rev" => Ok(Self::Rev),
401 "ref" | "&ref" => Ok(Self::Ref),
402 "submodules" | "&submodules" => Ok(Self::Submodules),
403 "shallow" | "&shallow" => Ok(Self::Shallow),
404 "lfs" | "&lfs" => Ok(Self::Lfs),
405 "exportIgnore" | "&exportIgnore" => Ok(Self::ExportIgnore),
406 "allRefs" | "&allRefs" => Ok(Self::AllRefs),
407 "verifyCommit" | "&verifyCommit" => Ok(Self::VerifyCommit),
408 "keytype" | "&keytype" => Ok(Self::Keytype),
409 "publicKey" | "&publicKey" => Ok(Self::PublicKey),
410 "publicKeys" | "&publicKeys" => Ok(Self::PublicKeys),
411 arbitrary => Ok(Self::Arbitrary(
415 arbitrary.strip_prefix('&').unwrap_or(arbitrary).into(),
416 )),
417 }
418 }
419}
420
421#[cfg(test)]
422mod inc_parse {
423 use super::*;
424 #[test]
425 fn no_str() {
426 let expected = LocationParameters::default();
427 let in_str = "";
428 let (outstr, (parsed_param, ref_rev)) =
429 LocationParameters::parse.parse_peek(in_str).unwrap();
430 assert_eq!("", outstr);
431 assert_eq!(expected, parsed_param);
432 assert_eq!(ref_rev, ParamRefRev::default());
433 }
434 #[test]
435 fn empty() {
436 let expected = LocationParameters::default();
437 let in_str = "";
438 let (rest, (output, ref_rev)) = LocationParameters::parse.parse_peek(in_str).unwrap();
439 assert_eq!("", rest);
440 assert_eq!(output, expected);
441 assert_eq!(ref_rev, ParamRefRev::default());
442 }
443 #[test]
444 fn empty_hash_terminated() {
445 let expected = LocationParameters::default();
446 let in_str = "#";
447 let (rest, (output, ref_rev)) = LocationParameters::parse.parse_peek(in_str).unwrap();
448 assert_eq!("#", rest);
449 assert_eq!(output, expected);
450 assert_eq!(ref_rev, ParamRefRev::default());
451 }
452 #[test]
453 fn dir() {
454 let mut expected = LocationParameters::default();
455 expected.dir(Some("foo".to_string()));
456
457 let in_str = "dir=foo";
458 let (rest, (output, _)) = LocationParameters::parse.parse_peek(in_str).unwrap();
459 assert_eq!("", rest);
460 assert_eq!(output, expected);
461
462 let in_str = "&dir=foo";
463 let (rest, (output, _)) = LocationParameters::parse.parse_peek(in_str).unwrap();
464 assert_eq!("", rest);
465 assert_eq!(output, expected);
466 let in_str = "dir=&dir=foo";
467 let (rest, (output, _)) = LocationParameters::parse.parse_peek(in_str).unwrap();
468 assert_eq!("", rest);
469 assert_eq!(output, expected);
470
471 expected.dir(Some(String::new()));
472 let in_str = "dir=";
473 let (rest, (output, _)) = LocationParameters::parse.parse_peek(in_str).unwrap();
474 assert_eq!("", rest);
475 assert_eq!(output, expected);
476 }
477 #[test]
478 fn dir_hash_term() {
479 let mut expected = LocationParameters::default();
480 expected.dir(Some("foo".to_string()));
481
482 let in_str = "dir=foo#fizz";
483 let (rest, (output, _)) = LocationParameters::parse.parse_peek(in_str).unwrap();
484 assert_eq!("#fizz", rest);
485 assert_eq!(output, expected);
486
487 let in_str = "&dir=foo#fizz";
488 let (rest, (output, _)) = LocationParameters::parse.parse_peek(in_str).unwrap();
489 assert_eq!("#fizz", rest);
490 assert_eq!(output, expected);
491 let in_str = "dir=&dir=foo#fizz";
492 let (rest, (output, _)) = LocationParameters::parse.parse_peek(in_str).unwrap();
493 assert_eq!("#fizz", rest);
494 assert_eq!(output, expected);
495
496 expected.dir(Some(String::new()));
497 let in_str = "dir=#fizz";
498 let (rest, (output, _)) = LocationParameters::parse.parse_peek(in_str).unwrap();
499 assert_eq!("#fizz", rest);
500 assert_eq!(output, expected);
501 }
502
503 #[test]
504 fn canonical_param_keys_round_trip() {
505 let mut expected = LocationParameters::default();
508 expected.set_nar_hash(Some("sha256-abc".into()));
509 expected.set_last_modified(Some("12345".into()));
510 expected.set_rev_count(Some("42".into()));
511
512 let in_str = "narHash=sha256-abc&lastModified=12345&revCount=42";
513 let (rest, (output, _)) = LocationParameters::parse.parse_peek(in_str).unwrap();
514 assert_eq!("", rest);
515 assert_eq!(output, expected);
516
517 assert_eq!(
520 output.to_string(),
521 "lastModified=12345&narHash=sha256-abc&revCount=42"
522 );
523 }
524
525 #[test]
526 fn snake_case_falls_through_to_arbitrary() {
527 let in_str = "nar_hash=sha256-abc";
530 let (rest, (output, _)) = LocationParameters::parse.parse_peek(in_str).unwrap();
531 assert_eq!("", rest);
532 let mut expected = LocationParameters::default();
534 expected.add_arbitrary(("nar_hash".into(), "sha256-abc".into()));
535 assert_eq!(output, expected);
536 }
537}
538
539#[cfg(test)]
540mod git_typed_params {
541 use crate::{FlakeRef, NixUriError};
546 use rstest::rstest;
547
548 #[rstest]
549 #[case("1", true)]
550 #[case("0", false)]
551 fn lfs_accepts_bool_forms(#[case] input: &str, #[case] expected: bool) {
552 let url = format!("git+ssh://example.com/repo?lfs={input}");
553 let parsed: FlakeRef = url.parse().unwrap();
554 assert_eq!(parsed.params().lfs, Some(expected));
555 }
556
557 #[rstest]
558 #[case("1", true)]
559 #[case("0", false)]
560 fn export_ignore_accepts_bool_forms(#[case] input: &str, #[case] expected: bool) {
561 let url = format!("git+ssh://example.com/repo?exportIgnore={input}");
562 let parsed: FlakeRef = url.parse().unwrap();
563 assert_eq!(parsed.params().export_ignore, Some(expected));
564 }
565
566 #[rstest]
567 #[case("1", true)]
568 #[case("0", false)]
569 fn all_refs_accepts_bool_forms(#[case] input: &str, #[case] expected: bool) {
570 let url = format!("git+ssh://example.com/repo?allRefs={input}");
571 let parsed: FlakeRef = url.parse().unwrap();
572 assert_eq!(parsed.params().all_refs, Some(expected));
573 }
574
575 #[rstest]
576 #[case("1", true)]
577 #[case("0", false)]
578 fn verify_commit_accepts_bool_forms(#[case] input: &str, #[case] expected: bool) {
579 let url = format!("git+ssh://example.com/repo?verifyCommit={input}");
580 let parsed: FlakeRef = url.parse().unwrap();
581 assert_eq!(parsed.params().verify_commit, Some(expected));
582 }
583
584 #[rstest]
585 #[case("lfs", "yes")]
586 #[case("exportIgnore", "yes")]
587 #[case("allRefs", "yes")]
588 #[case("verifyCommit", "yes")]
589 #[case("lfs", "true")]
594 #[case("exportIgnore", "false")]
595 #[case("allRefs", "True")]
596 #[case("verifyCommit", "TRUE")]
597 fn bool_keys_reject_non_bool_values(#[case] key: &str, #[case] value: &str) {
598 let url = format!("git+ssh://example.com/repo?{key}={value}");
599 let err = url.parse::<FlakeRef>().unwrap_err();
600 match err {
601 NixUriError::InvalidValue { field, .. } => {
602 assert_eq!(field, key, "expected error.field to name the rejected key");
603 }
604 other => panic!("expected InvalidValue, got {other:?}"),
605 }
606 }
607
608 #[test]
609 fn keytype_routes_to_typed_slot() {
610 let parsed: FlakeRef = "git+ssh://example.com/repo?keytype=ssh-ed25519"
611 .parse()
612 .unwrap();
613 assert_eq!(parsed.params().keytype.as_deref(), Some("ssh-ed25519"));
614 }
615
616 #[test]
617 fn public_key_routes_to_typed_slot() {
618 let parsed: FlakeRef = "git+ssh://example.com/repo?publicKey=abcdef"
619 .parse()
620 .unwrap();
621 assert_eq!(parsed.params().public_key.as_deref(), Some("abcdef"));
622 }
623
624 #[test]
625 fn public_keys_routes_to_typed_slot() {
626 let parsed: FlakeRef = "git+ssh://example.com/repo?publicKeys=k1.k2.k3"
627 .parse()
628 .unwrap();
629 assert_eq!(parsed.params().public_keys.as_deref(), Some("k1.k2.k3"));
630 }
631
632 #[test]
633 fn display_emits_seven_keys_alphabetically() {
634 let url = "git+ssh://example.com/repo?\
638 verifyCommit=1&publicKeys=k1.k2&publicKey=abc&\
639 narHash=sha256-x&lfs=1&keytype=ssh-ed25519&\
640 exportIgnore=0&allRefs=1";
641 let parsed: FlakeRef = url.parse().unwrap();
642 let expected = "git+ssh://example.com/repo?\
643 allRefs=1&exportIgnore=0&keytype=ssh-ed25519&\
644 lfs=1&narHash=sha256-x&publicKey=abc&\
645 publicKeys=k1.k2&verifyCommit=1";
646 assert_eq!(parsed.to_string(), expected);
647 }
648
649 #[rstest]
650 #[case("1", true)]
651 #[case("0", false)]
652 fn submodules_accepts_bool_forms(#[case] input: &str, #[case] expected: bool) {
653 let url = format!("git+ssh://example.com/repo?submodules={input}");
654 let parsed: FlakeRef = url.parse().unwrap();
655 assert_eq!(parsed.params().submodules, Some(expected));
656 }
657
658 #[rstest]
659 #[case("1", true)]
660 #[case("0", false)]
661 fn shallow_accepts_bool_forms(#[case] input: &str, #[case] expected: bool) {
662 let url = format!("git+ssh://example.com/repo?shallow={input}");
663 let parsed: FlakeRef = url.parse().unwrap();
664 assert_eq!(parsed.params().shallow, Some(expected));
665 }
666
667 #[rstest]
668 #[case("submodules", "garbage")]
669 #[case("submodules", "true")]
670 #[case("shallow", "garbage")]
671 #[case("shallow", "false")]
672 fn submodules_shallow_reject_non_bool_values(#[case] key: &str, #[case] value: &str) {
673 let url = format!("git+ssh://example.com/repo?{key}={value}");
677 let err = url.parse::<FlakeRef>().unwrap_err();
678 match err {
679 NixUriError::InvalidValue { field, .. } => assert_eq!(field, key),
680 other => panic!("expected InvalidValue, got {other:?}"),
681 }
682 }
683
684 #[test]
685 fn submodules_round_trips_canonically() {
686 let url = "git+ssh://example.com/repo?submodules=1";
687 let parsed: FlakeRef = url.parse().unwrap();
688 assert_eq!(parsed.to_string(), url);
689 assert_eq!(parsed.params().submodules, Some(true));
690 }
691
692 #[test]
693 fn shallow_round_trips_canonically() {
694 let url = "git+ssh://example.com/repo?shallow=1";
695 let parsed: FlakeRef = url.parse().unwrap();
696 assert_eq!(parsed.to_string(), url);
697 assert_eq!(parsed.params().shallow, Some(true));
698 }
699
700 #[test]
701 fn round_trip_realistic_git_url() {
702 let url = "git+ssh://example.com/repo?allRefs=1&lfs=1&publicKey=abc";
703 let parsed: FlakeRef = url.parse().unwrap();
704 assert_eq!(parsed.params().all_refs, Some(true));
705 assert_eq!(parsed.params().lfs, Some(true));
706 assert_eq!(parsed.params().public_key.as_deref(), Some("abc"));
707 assert_eq!(parsed.to_string(), url);
708 }
709}