1use std::collections::HashMap;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Deserializer, Serialize};
5
6use super::{
7 ArchiveHooksConfig, SignConfig, StringOrBool, StringOrU32, deserialize_string_or_bool_opt,
8};
9
10#[derive(Debug, Clone, JsonSchema)]
15pub enum ArchivesConfig {
16 Disabled,
17 Configs(Vec<ArchiveConfig>),
18}
19
20impl Serialize for ArchivesConfig {
21 fn serialize<S: serde::Serializer>(
22 &self,
23 serializer: S,
24 ) -> std::result::Result<S::Ok, S::Error> {
25 match self {
26 ArchivesConfig::Disabled => serializer.serialize_bool(false),
27 ArchivesConfig::Configs(configs) => configs.serialize(serializer),
28 }
29 }
30}
31
32impl Default for ArchivesConfig {
33 fn default() -> Self {
34 ArchivesConfig::Configs(vec![])
35 }
36}
37
38pub(super) fn deserialize_archives_config<'de, D>(
44 deserializer: D,
45) -> Result<ArchivesConfig, D::Error>
46where
47 D: Deserializer<'de>,
48{
49 use serde::de::{self, Visitor};
50
51 struct ArchivesVisitor;
52
53 impl<'de> Visitor<'de> for ArchivesVisitor {
54 type Value = ArchivesConfig;
55
56 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.write_str("false or a list of archive configs")
58 }
59
60 fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
61 if !v {
62 Ok(ArchivesConfig::Disabled)
63 } else {
64 Err(E::custom(
65 "archives: true is not valid; use false or a list",
66 ))
67 }
68 }
69
70 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
71 let mut configs = Vec::new();
72 while let Some(item) = seq.next_element::<ArchiveConfig>()? {
73 configs.push(item);
74 }
75 Ok(ArchivesConfig::Configs(configs))
76 }
77
78 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
80 Ok(ArchivesConfig::Configs(vec![]))
81 }
82
83 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
84 Ok(ArchivesConfig::Configs(vec![]))
85 }
86 }
87
88 deserializer.deserialize_any(ArchivesVisitor)
89}
90
91pub(super) fn deserialize_signs<'de, D>(deserializer: D) -> Result<Vec<SignConfig>, D::Error>
97where
98 D: Deserializer<'de>,
99{
100 use serde::de::{self, Visitor};
101
102 struct SignsVisitor;
103
104 impl<'de> Visitor<'de> for SignsVisitor {
105 type Value = Vec<SignConfig>;
106
107 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 f.write_str("a sign config object or an array of sign config objects")
109 }
110
111 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
112 let mut configs = Vec::new();
113 while let Some(item) = seq.next_element::<SignConfig>()? {
114 configs.push(item);
115 }
116 Ok(configs)
117 }
118
119 fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
120 let config = SignConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
121 Ok(vec![config])
122 }
123
124 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
125 Ok(Vec::new())
126 }
127
128 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
129 Ok(Vec::new())
130 }
131 }
132
133 deserializer.deserialize_any(SignsVisitor)
134}
135
136pub(super) fn deserialize_binary_signs<'de, D>(deserializer: D) -> Result<Vec<SignConfig>, D::Error>
155where
156 D: Deserializer<'de>,
157{
158 let configs = deserialize_signs(deserializer)?;
159 for (idx, cfg) in configs.iter().enumerate() {
160 if let Some(art) = cfg.artifacts.as_deref()
161 && art != "binary"
162 && art != "none"
163 {
164 return Err(serde::de::Error::custom(format!(
165 "binary_signs[{idx}].artifacts: '{art}' is not allowed; \
166 binary_signs accepts only 'binary' or 'none' (use top-level \
167 `signs:` for broader artifact filters)"
168 )));
169 }
170 }
171 Ok(configs)
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
179#[serde(untagged)]
180pub enum WrapInDirectory {
181 Bool(bool),
182 Name(String),
183}
184
185impl<'de> serde::Deserialize<'de> for WrapInDirectory {
186 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
187 let value = serde_yaml_ng::Value::deserialize(deserializer)?;
188 match value {
189 serde_yaml_ng::Value::Bool(b) => Ok(WrapInDirectory::Bool(b)),
190 serde_yaml_ng::Value::String(s) => Ok(WrapInDirectory::Name(s)),
191 _ => Err(serde::de::Error::custom("expected bool or string")),
192 }
193 }
194}
195
196impl WrapInDirectory {
197 pub fn directory_name(&self, default_name: &str) -> Option<String> {
203 match self {
204 WrapInDirectory::Bool(true) => Some(default_name.to_string()),
205 WrapInDirectory::Bool(false) => None,
206 WrapInDirectory::Name(s) if s.is_empty() => None,
207 WrapInDirectory::Name(s) => Some(s.clone()),
208 }
209 }
210}
211
212#[derive(Debug, Clone, Serialize, Default, JsonSchema)]
217#[serde(deny_unknown_fields)]
218pub struct ArchiveConfig {
219 pub id: Option<String>,
223 pub name_template: Option<String>,
225 pub formats: Option<Vec<String>>,
230 pub format_overrides: Option<Vec<FormatOverride>>,
232 pub files: Option<Vec<ArchiveFileSpec>>,
234 pub binaries: Option<Vec<String>>,
236 pub wrap_in_directory: Option<WrapInDirectory>,
240 pub ids: Option<Vec<String>>,
242 pub meta: Option<bool>,
244 pub builds_info: Option<ArchiveFileInfo>,
246 pub strip_binary_directory: Option<bool>,
248 pub allow_different_binary_count: Option<bool>,
250 pub hooks: Option<ArchiveHooksConfig>,
252 pub templated_files: Option<Vec<super::TemplateFileConfig>>,
257 #[serde(rename = "if")]
264 pub if_condition: Option<String>,
265 pub completions: Option<super::CompletionsConfig>,
269 pub manpages: Option<super::ManpagesConfig>,
273}
274
275fn fold_format_into_formats(
284 context_label: &str,
285 context_kind: &str,
286 formats: Option<Vec<String>>,
287 legacy: Option<String>,
288) -> Option<Vec<String>> {
289 let mut formats = formats;
290 if let Some(legacy) = legacy {
291 tracing::warn!(
292 "DEPRECATION: {}[{}]: 'format: {}' is deprecated; \
293 use 'formats: [{}]' instead.",
294 context_kind,
295 context_label,
296 legacy,
297 legacy
298 );
299 formats.get_or_insert_with(Vec::new).push(legacy);
300 }
301 formats
302}
303
304impl<'de> Deserialize<'de> for ArchiveConfig {
311 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
312 where
313 D: Deserializer<'de>,
314 {
315 #[derive(Deserialize, Default)]
316 #[serde(default, deny_unknown_fields)]
317 struct Raw {
318 id: Option<String>,
319 name_template: Option<String>,
320 formats: Option<Vec<String>>,
321 format: Option<String>,
322 format_overrides: Option<Vec<FormatOverride>>,
323 files: Option<Vec<ArchiveFileSpec>>,
324 binaries: Option<Vec<String>>,
325 wrap_in_directory: Option<WrapInDirectory>,
326 ids: Option<Vec<String>>,
327 builds: Option<Vec<String>>,
328 meta: Option<bool>,
329 builds_info: Option<ArchiveFileInfo>,
330 strip_binary_directory: Option<bool>,
331 allow_different_binary_count: Option<bool>,
332 hooks: Option<ArchiveHooksConfig>,
333 templated_files: Option<Vec<super::TemplateFileConfig>>,
334 #[serde(rename = "if")]
335 if_condition: Option<String>,
336 completions: Option<super::CompletionsConfig>,
337 manpages: Option<super::ManpagesConfig>,
338 }
339
340 let raw = Raw::deserialize(deserializer)?;
341
342 let id_label = raw.id.clone().unwrap_or_else(|| "default".to_string());
343 let formats = fold_format_into_formats(
344 &format!("id={}", id_label),
345 "archives",
346 raw.formats,
347 raw.format,
348 );
349 let mut ids = raw.ids;
350 if let Some(legacy) = raw.builds {
351 tracing::warn!(
352 "DEPRECATION: archives[id={}]: 'builds: {:?}' is deprecated; \
353 use 'ids: [...]' instead.",
354 id_label,
355 legacy
356 );
357 let target = ids.get_or_insert_with(Vec::new);
358 target.extend(legacy);
359 }
360
361 Ok(ArchiveConfig {
362 id: raw.id.or_else(|| Some("default".to_string())),
363 name_template: raw.name_template,
364 formats,
365 format_overrides: raw.format_overrides,
366 files: raw.files,
367 binaries: raw.binaries,
368 wrap_in_directory: raw.wrap_in_directory,
369 ids,
370 meta: raw.meta,
371 builds_info: raw.builds_info,
372 strip_binary_directory: raw.strip_binary_directory,
373 allow_different_binary_count: raw.allow_different_binary_count,
374 hooks: raw.hooks,
375 templated_files: raw.templated_files,
376 if_condition: raw.if_condition,
377 completions: raw.completions,
378 manpages: raw.manpages,
379 })
380 }
381}
382
383#[derive(Debug, Clone, Serialize, JsonSchema)]
384#[serde(deny_unknown_fields)]
385pub struct FormatOverride {
386 pub os: String,
388 pub formats: Option<Vec<String>>,
391}
392
393impl<'de> Deserialize<'de> for FormatOverride {
398 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
399 where
400 D: Deserializer<'de>,
401 {
402 #[derive(Deserialize, Default)]
403 #[serde(default, deny_unknown_fields)]
404 struct Raw {
405 os: String,
406 formats: Option<Vec<String>>,
407 format: Option<String>,
408 }
409 let raw = Raw::deserialize(deserializer)?;
410 let formats = fold_format_into_formats(
411 &format!("os={}", raw.os),
412 "archives.format_overrides",
413 raw.formats,
414 raw.format,
415 );
416 Ok(FormatOverride {
417 os: raw.os,
418 formats,
419 })
420 }
421}
422
423#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
434#[serde(untagged)]
435pub enum ArchiveFileSpec {
436 Glob(String),
437 Detailed {
438 src: String,
439 dst: Option<String>,
440 info: Option<ArchiveFileInfo>,
441 strip_parent: Option<bool>,
443 },
444}
445
446impl PartialEq<&str> for ArchiveFileSpec {
447 fn eq(&self, other: &&str) -> bool {
448 match self {
449 ArchiveFileSpec::Glob(s) => s.as_str() == *other,
450 _ => false,
451 }
452 }
453}
454
455#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
459#[serde(default, deny_unknown_fields)]
460pub struct FileInfo {
461 pub owner: Option<String>,
463 pub group: Option<String>,
465 pub mode: Option<StringOrU32>,
470 pub mtime: Option<String>,
472}
473
474pub type ArchiveFileInfo = FileInfo;
476
477pub fn parse_octal_mode(s: &str) -> Option<u32> {
480 let cleaned = s
481 .strip_prefix("0o")
482 .or_else(|| s.strip_prefix("0O"))
483 .unwrap_or(s);
484 let cleaned = if cleaned.is_empty() { "0" } else { cleaned };
485 u32::from_str_radix(cleaned, 8).ok()
486}
487
488pub const VALID_ARCHIVE_FORMATS: &[&str] = &[
492 "tar.gz", "tgz", "tar.xz", "txz", "tar.zst", "tzst", "tar", "zip", "gz", "xz", "binary", "none",
493];
494
495#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
504#[serde(untagged)]
505pub enum ExtraFileSpec {
506 Glob(String),
507 Detailed {
508 glob: String,
509 #[serde(default)]
511 name_template: Option<String>,
512 #[serde(default)]
518 allow_empty: bool,
519 },
520}
521
522impl ExtraFileSpec {
523 pub fn glob(&self) -> &str {
525 match self {
526 ExtraFileSpec::Glob(s) => s,
527 ExtraFileSpec::Detailed { glob, .. } => glob,
528 }
529 }
530
531 pub fn name_template(&self) -> Option<&str> {
533 match self {
534 ExtraFileSpec::Glob(_) => None,
535 ExtraFileSpec::Detailed { name_template, .. } => name_template.as_deref(),
536 }
537 }
538
539 pub fn allow_empty(&self) -> bool {
542 match self {
543 ExtraFileSpec::Glob(_) => false,
544 ExtraFileSpec::Detailed { allow_empty, .. } => *allow_empty,
545 }
546 }
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
552#[serde(default, deny_unknown_fields)]
553pub struct TemplatedExtraFile {
554 pub src: String,
556 pub dst: Option<String>,
559 pub mode: Option<String>,
562}
563
564#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
566#[serde(rename_all = "lowercase")]
567pub enum ChecksumSplitFormat {
568 #[default]
571 Bare,
572 Coreutils,
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
579#[serde(default, deny_unknown_fields)]
580pub struct ChecksumConfig {
581 pub name_template: Option<String>,
583 pub algorithm: Option<String>,
589 #[serde(
592 alias = "disable",
593 deserialize_with = "deserialize_string_or_bool_opt",
594 default
595 )]
596 pub skip: Option<StringOrBool>,
597 pub extra_files: Option<Vec<ExtraFileSpec>>,
599 pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
602 pub ids: Option<Vec<String>>,
604 pub split: Option<bool>,
606 pub split_format: Option<ChecksumSplitFormat>,
611}
612
613impl ChecksumConfig {
614 pub const DEFAULT_NAME_TEMPLATE: &'static str = "{{ ProjectName }}_{{ Version }}_checksums.txt";
617
618 pub const DEFAULT_ALGORITHM: &'static str = "sha256";
620
621 pub const SUPPORTED_ALGORITHMS: &'static [&'static str] = &[
627 "sha1", "sha224", "sha256", "sha384", "sha512", "sha3-224", "sha3-256", "sha3-384",
628 "sha3-512", "blake2b", "blake2s", "blake3", "crc32", "md5",
629 ];
630
631 pub fn resolved_algorithm(&self) -> &str {
636 self.algorithm.as_deref().unwrap_or(Self::DEFAULT_ALGORITHM)
637 }
638
639 pub fn resolved_split(&self) -> bool {
642 self.split.unwrap_or(false)
643 }
644
645 pub fn resolved_combined_name_template(&self) -> &str {
653 self.name_template
654 .as_deref()
655 .unwrap_or(Self::DEFAULT_NAME_TEMPLATE)
656 }
657
658 pub fn resolve_combined_name_template<'a>(
666 crate_checksum: Option<&'a ChecksumConfig>,
667 global_checksum: Option<&'a ChecksumConfig>,
668 ) -> &'a str {
669 crate_checksum
670 .and_then(|c| c.name_template.as_deref())
671 .or_else(|| global_checksum.and_then(|c| c.name_template.as_deref()))
672 .unwrap_or(Self::DEFAULT_NAME_TEMPLATE)
673 }
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
701#[serde(untagged)]
702pub enum ContentSource {
703 Inline(String),
704 FromFile {
705 from_file: String,
706 },
707 FromUrl {
708 from_url: String,
709 #[serde(default, skip_serializing_if = "Option::is_none")]
712 headers: Option<HashMap<String, String>>,
713 },
714}
715
716impl PartialEq for ContentSource {
717 fn eq(&self, other: &Self) -> bool {
718 match (self, other) {
719 (Self::Inline(a), Self::Inline(b)) => a == b,
720 (Self::FromFile { from_file: a }, Self::FromFile { from_file: b }) => a == b,
721 (
722 Self::FromUrl {
723 from_url: a,
724 headers: ha,
725 },
726 Self::FromUrl {
727 from_url: b,
728 headers: hb,
729 },
730 ) => a == b && ha == hb,
731 _ => false,
732 }
733 }
734}
735
736#[cfg(test)]
737mod tests {
738 use super::*;
739
740 #[derive(Deserialize)]
746 struct ArchivesWrapper {
747 #[serde(default, deserialize_with = "deserialize_archives_config")]
748 archives: ArchivesConfig,
749 }
750
751 #[test]
752 fn archives_false_is_disabled() {
753 let w: ArchivesWrapper = serde_yaml_ng::from_str("archives: false").unwrap();
754 assert!(matches!(w.archives, ArchivesConfig::Disabled));
755 }
756
757 #[test]
758 fn archives_true_is_rejected() {
759 let r: Result<ArchivesWrapper, _> = serde_yaml_ng::from_str("archives: true");
761 assert!(r.is_err(), "archives: true must be rejected");
762 }
763
764 #[test]
765 fn archives_list_becomes_configs() {
766 let w: ArchivesWrapper =
767 serde_yaml_ng::from_str("archives:\n - id: a\n - id: b\n").unwrap();
768 match w.archives {
769 ArchivesConfig::Configs(c) => assert_eq!(c.len(), 2),
770 other => panic!("expected Configs, got {other:?}"),
771 }
772 }
773
774 #[test]
775 fn archives_null_defaults_to_empty_configs() {
776 let w: ArchivesWrapper = serde_yaml_ng::from_str("archives: null").unwrap();
777 match w.archives {
778 ArchivesConfig::Configs(c) => assert!(c.is_empty()),
779 other => panic!("expected empty Configs, got {other:?}"),
780 }
781 }
782
783 #[derive(Deserialize)]
784 struct SignsWrapper {
785 #[serde(default, deserialize_with = "deserialize_signs")]
786 signs: Vec<SignConfig>,
787 }
788
789 #[test]
790 fn signs_single_object_becomes_one_element_vec() {
791 let w: SignsWrapper = serde_yaml_ng::from_str("signs:\n artifacts: all\n").unwrap();
793 assert_eq!(w.signs.len(), 1);
794 assert_eq!(w.signs[0].artifacts.as_deref(), Some("all"));
795 }
796
797 #[test]
798 fn signs_sequence_collects_all() {
799 let w: SignsWrapper =
800 serde_yaml_ng::from_str("signs:\n - artifacts: all\n - artifacts: checksum\n")
801 .unwrap();
802 assert_eq!(w.signs.len(), 2);
803 }
804
805 #[test]
806 fn signs_null_is_empty_vec() {
807 let w: SignsWrapper = serde_yaml_ng::from_str("signs: null").unwrap();
808 assert!(w.signs.is_empty());
809 }
810
811 #[derive(Deserialize)]
812 struct BinarySignsWrapper {
813 #[serde(default, deserialize_with = "deserialize_binary_signs")]
814 binary_signs: Vec<SignConfig>,
815 }
816
817 #[test]
818 fn binary_signs_accepts_binary_and_none() {
819 let w: BinarySignsWrapper =
820 serde_yaml_ng::from_str("binary_signs:\n - artifacts: binary\n").unwrap();
821 assert_eq!(w.binary_signs.len(), 1);
822 let w2: BinarySignsWrapper =
823 serde_yaml_ng::from_str("binary_signs:\n - artifacts: none\n").unwrap();
824 assert_eq!(w2.binary_signs.len(), 1);
825 }
826
827 #[test]
828 fn binary_signs_rejects_broad_artifact_filter() {
829 let r: Result<BinarySignsWrapper, _> =
831 serde_yaml_ng::from_str("binary_signs:\n - artifacts: all\n");
832 assert!(
833 r.is_err(),
834 "binary_signs must reject a non-binary artifact filter"
835 );
836 }
837
838 #[test]
841 fn wrap_in_directory_bool_true_uses_default_name() {
842 assert_eq!(
843 WrapInDirectory::Bool(true).directory_name("myapp_1.0"),
844 Some("myapp_1.0".to_string())
845 );
846 }
847
848 #[test]
849 fn wrap_in_directory_bool_false_disables_wrapping() {
850 assert_eq!(
851 WrapInDirectory::Bool(false).directory_name("myapp_1.0"),
852 None
853 );
854 }
855
856 #[test]
857 fn wrap_in_directory_empty_string_disables_wrapping() {
858 assert_eq!(
860 WrapInDirectory::Name(String::new()).directory_name("fallback"),
861 None
862 );
863 }
864
865 #[test]
866 fn wrap_in_directory_custom_name_overrides_default() {
867 assert_eq!(
868 WrapInDirectory::Name("custom".into()).directory_name("fallback"),
869 Some("custom".to_string())
870 );
871 }
872
873 #[derive(Deserialize)]
874 struct WrapWrapper {
875 wrap_in_directory: WrapInDirectory,
876 }
877
878 #[test]
879 fn wrap_in_directory_deserializes_bool_and_string() {
880 let b: WrapWrapper = serde_yaml_ng::from_str("wrap_in_directory: true").unwrap();
881 assert_eq!(b.wrap_in_directory, WrapInDirectory::Bool(true));
882 let s: WrapWrapper = serde_yaml_ng::from_str("wrap_in_directory: dist").unwrap();
883 assert_eq!(s.wrap_in_directory, WrapInDirectory::Name("dist".into()));
884 }
885
886 #[test]
887 fn wrap_in_directory_rejects_non_scalar() {
888 let r: Result<WrapWrapper, _> = serde_yaml_ng::from_str("wrap_in_directory:\n - a\n");
891 assert!(r.is_err());
892 }
893
894 #[test]
897 fn parse_octal_mode_accepts_common_forms() {
898 assert_eq!(parse_octal_mode("0755"), Some(0o755));
899 assert_eq!(parse_octal_mode("0o755"), Some(0o755));
900 assert_eq!(parse_octal_mode("0O755"), Some(0o755));
901 assert_eq!(parse_octal_mode("755"), Some(0o755));
902 assert_eq!(parse_octal_mode("0o"), Some(0));
905 assert_eq!(parse_octal_mode("0"), Some(0));
906 }
907
908 #[test]
909 fn parse_octal_mode_rejects_non_octal() {
910 assert_eq!(parse_octal_mode("0o899"), None);
912 assert_eq!(parse_octal_mode("garbage"), None);
913 }
914
915 #[test]
918 fn resolve_combined_name_template_prefers_crate_then_global_then_default() {
919 let crate_cfg = ChecksumConfig {
920 name_template: Some("crate.txt".into()),
921 ..Default::default()
922 };
923 let global_cfg = ChecksumConfig {
924 name_template: Some("global.txt".into()),
925 ..Default::default()
926 };
927 assert_eq!(
929 ChecksumConfig::resolve_combined_name_template(Some(&crate_cfg), Some(&global_cfg)),
930 "crate.txt"
931 );
932 let bare = ChecksumConfig::default();
934 assert_eq!(
935 ChecksumConfig::resolve_combined_name_template(Some(&bare), Some(&global_cfg)),
936 "global.txt"
937 );
938 assert_eq!(
940 ChecksumConfig::resolve_combined_name_template(None, None),
941 ChecksumConfig::DEFAULT_NAME_TEMPLATE
942 );
943 }
944
945 #[test]
948 fn checksum_split_format_defaults_to_bare() {
949 assert_eq!(ChecksumSplitFormat::default(), ChecksumSplitFormat::Bare);
950 }
951
952 #[test]
953 fn checksum_split_format_deserializes_lowercase() {
954 assert_eq!(
955 serde_yaml_ng::from_str::<ChecksumSplitFormat>("bare").unwrap(),
956 ChecksumSplitFormat::Bare
957 );
958 assert_eq!(
959 serde_yaml_ng::from_str::<ChecksumSplitFormat>("coreutils").unwrap(),
960 ChecksumSplitFormat::Coreutils
961 );
962 assert!(serde_yaml_ng::from_str::<ChecksumSplitFormat>("Coreutils").is_err());
963 }
964
965 #[test]
968 fn extra_file_spec_allow_empty_only_true_for_detailed_opt_in() {
969 let bare: ExtraFileSpec = serde_yaml_ng::from_str("dist/*.sig").unwrap();
971 assert!(!bare.allow_empty());
972 let opt_in: ExtraFileSpec =
974 serde_yaml_ng::from_str("glob: keys/*.pub\nallow_empty: true").unwrap();
975 assert!(opt_in.allow_empty());
976 assert_eq!(opt_in.glob(), "keys/*.pub");
977 let default_off: ExtraFileSpec = serde_yaml_ng::from_str("glob: docs/*.pdf").unwrap();
979 assert!(!default_off.allow_empty());
980 }
981
982 #[test]
985 fn archive_file_spec_str_eq_matches_glob_only() {
986 assert!(ArchiveFileSpec::Glob("README.md".into()) == "README.md");
987 assert!(ArchiveFileSpec::Glob("README.md".into()) != "other");
988 let detailed = ArchiveFileSpec::Detailed {
990 src: "README.md".into(),
991 dst: None,
992 info: None,
993 strip_parent: None,
994 };
995 assert!(detailed != "README.md");
996 }
997
998 #[test]
1001 fn archive_config_folds_singular_format_into_formats() {
1002 let c: ArchiveConfig = serde_yaml_ng::from_str("format: tar.gz").unwrap();
1004 assert_eq!(c.formats.as_deref().unwrap(), ["tar.gz"]);
1005 }
1006
1007 #[test]
1008 fn archive_config_folds_deprecated_builds_into_ids() {
1009 let c: ArchiveConfig = serde_yaml_ng::from_str("ids: [keep]\nbuilds: [legacy]").unwrap();
1010 let ids = c.ids.unwrap();
1011 assert!(ids.contains(&"keep".to_string()));
1012 assert!(ids.contains(&"legacy".to_string()));
1013 }
1014
1015 #[test]
1016 fn archive_config_defaults_id_to_default() {
1017 let c: ArchiveConfig = serde_yaml_ng::from_str("name_template: x").unwrap();
1020 assert_eq!(c.id.as_deref(), Some("default"));
1021 let named: ArchiveConfig = serde_yaml_ng::from_str("id: bins").unwrap();
1023 assert_eq!(named.id.as_deref(), Some("bins"));
1024 }
1025
1026 #[test]
1027 fn format_override_folds_singular_format() {
1028 let o: FormatOverride = serde_yaml_ng::from_str("os: windows\nformat: zip").unwrap();
1029 assert_eq!(o.os, "windows");
1030 assert_eq!(o.formats.as_deref().unwrap(), ["zip"]);
1031 }
1032
1033 #[test]
1036 fn content_source_partial_eq_by_variant_and_payload() {
1037 assert_eq!(
1038 ContentSource::Inline("a".into()),
1039 ContentSource::Inline("a".into())
1040 );
1041 assert_ne!(
1042 ContentSource::Inline("a".into()),
1043 ContentSource::Inline("b".into())
1044 );
1045 assert_ne!(
1047 ContentSource::Inline("a".into()),
1048 ContentSource::FromFile {
1049 from_file: "a".into()
1050 }
1051 );
1052 let mut h = HashMap::new();
1054 h.insert("Accept".to_string(), "text/plain".to_string());
1055 let with_headers = ContentSource::FromUrl {
1056 from_url: "u".into(),
1057 headers: Some(h.clone()),
1058 };
1059 assert_eq!(
1060 with_headers,
1061 ContentSource::FromUrl {
1062 from_url: "u".into(),
1063 headers: Some(h),
1064 }
1065 );
1066 assert_ne!(
1067 with_headers,
1068 ContentSource::FromUrl {
1069 from_url: "u".into(),
1070 headers: None,
1071 }
1072 );
1073 }
1074
1075 #[test]
1076 fn content_source_from_url_deserializes_headers() {
1077 let cs: ContentSource = serde_yaml_ng::from_str(
1078 "from_url: https://example.com/h.md\nheaders:\n X-Token: abc\n",
1079 )
1080 .unwrap();
1081 match cs {
1082 ContentSource::FromUrl { from_url, headers } => {
1083 assert_eq!(from_url, "https://example.com/h.md");
1084 assert_eq!(headers.unwrap().get("X-Token").unwrap(), "abc");
1085 }
1086 other => panic!("expected FromUrl, got {other:?}"),
1087 }
1088 }
1089
1090 #[test]
1093 fn templated_extra_file_parses_and_defaults() {
1094 let full: TemplatedExtraFile = serde_yaml_ng::from_str(
1095 "src: NOTES.tera\ndst: \"{{ ProjectName }}-NOTES.txt\"\nmode: \"0644\"",
1096 )
1097 .unwrap();
1098 assert_eq!(full.src, "NOTES.tera");
1099 assert_eq!(full.dst.as_deref(), Some("{{ ProjectName }}-NOTES.txt"));
1100 assert_eq!(full.mode.as_deref(), Some("0644"));
1101 let minimal: TemplatedExtraFile = serde_yaml_ng::from_str("src: NOTES.tera").unwrap();
1103 assert!(minimal.dst.is_none());
1104 assert!(minimal.mode.is_none());
1105 assert!(serde_yaml_ng::from_str::<TemplatedExtraFile>("src: x\nbogus: y").is_err());
1107 }
1108}