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")]
266 pub if_condition: Option<String>,
267 pub completions: Option<super::CompletionsConfig>,
271 pub manpages: Option<super::ManpagesConfig>,
275}
276
277fn fold_format_into_formats(
286 context_label: &str,
287 context_kind: &str,
288 formats: Option<Vec<String>>,
289 legacy: Option<String>,
290) -> Option<Vec<String>> {
291 let mut formats = formats;
292 if let Some(legacy) = legacy {
293 tracing::warn!(
294 "DEPRECATION: {}[{}]: 'format: {}' is deprecated; \
295 use 'formats: [{}]' instead.",
296 context_kind,
297 context_label,
298 legacy,
299 legacy
300 );
301 formats.get_or_insert_with(Vec::new).push(legacy);
302 }
303 formats
304}
305
306impl<'de> Deserialize<'de> for ArchiveConfig {
313 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
314 where
315 D: Deserializer<'de>,
316 {
317 #[derive(Deserialize, Default)]
318 #[serde(default, deny_unknown_fields)]
319 struct Raw {
320 id: Option<String>,
321 name_template: Option<String>,
322 formats: Option<Vec<String>>,
323 format: Option<String>,
324 format_overrides: Option<Vec<FormatOverride>>,
325 files: Option<Vec<ArchiveFileSpec>>,
326 binaries: Option<Vec<String>>,
327 wrap_in_directory: Option<WrapInDirectory>,
328 ids: Option<Vec<String>>,
329 builds: Option<Vec<String>>,
330 meta: Option<bool>,
331 builds_info: Option<ArchiveFileInfo>,
332 strip_binary_directory: Option<bool>,
333 allow_different_binary_count: Option<bool>,
334 hooks: Option<ArchiveHooksConfig>,
335 templated_files: Option<Vec<super::TemplateFileConfig>>,
336 #[serde(rename = "if")]
337 if_condition: Option<String>,
338 completions: Option<super::CompletionsConfig>,
339 manpages: Option<super::ManpagesConfig>,
340 }
341
342 let raw = Raw::deserialize(deserializer)?;
343
344 let id_label = raw.id.clone().unwrap_or_else(|| "default".to_string());
345 let formats = fold_format_into_formats(
346 &format!("id={}", id_label),
347 "archives",
348 raw.formats,
349 raw.format,
350 );
351 let mut ids = raw.ids;
352 if let Some(legacy) = raw.builds {
353 tracing::warn!(
354 "DEPRECATION: archives[id={}]: 'builds: {:?}' is deprecated; \
355 use 'ids: [...]' instead.",
356 id_label,
357 legacy
358 );
359 let target = ids.get_or_insert_with(Vec::new);
360 target.extend(legacy);
361 }
362
363 Ok(ArchiveConfig {
364 id: raw.id.or_else(|| Some("default".to_string())),
365 name_template: raw.name_template,
366 formats,
367 format_overrides: raw.format_overrides,
368 files: raw.files,
369 binaries: raw.binaries,
370 wrap_in_directory: raw.wrap_in_directory,
371 ids,
372 meta: raw.meta,
373 builds_info: raw.builds_info,
374 strip_binary_directory: raw.strip_binary_directory,
375 allow_different_binary_count: raw.allow_different_binary_count,
376 hooks: raw.hooks,
377 templated_files: raw.templated_files,
378 if_condition: raw.if_condition,
379 completions: raw.completions,
380 manpages: raw.manpages,
381 })
382 }
383}
384
385#[derive(Debug, Clone, Serialize, JsonSchema)]
386#[serde(deny_unknown_fields)]
387pub struct FormatOverride {
388 pub os: String,
390 pub formats: Option<Vec<String>>,
393}
394
395impl<'de> Deserialize<'de> for FormatOverride {
400 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
401 where
402 D: Deserializer<'de>,
403 {
404 #[derive(Deserialize, Default)]
405 #[serde(default, deny_unknown_fields)]
406 struct Raw {
407 os: String,
408 formats: Option<Vec<String>>,
409 format: Option<String>,
410 }
411 let raw = Raw::deserialize(deserializer)?;
412 let formats = fold_format_into_formats(
413 &format!("os={}", raw.os),
414 "archives.format_overrides",
415 raw.formats,
416 raw.format,
417 );
418 Ok(FormatOverride {
419 os: raw.os,
420 formats,
421 })
422 }
423}
424
425#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
436#[serde(untagged)]
437pub enum ArchiveFileSpec {
438 Glob(String),
439 Detailed {
440 src: String,
441 dst: Option<String>,
442 info: Option<ArchiveFileInfo>,
443 strip_parent: Option<bool>,
445 },
446}
447
448impl PartialEq<&str> for ArchiveFileSpec {
449 fn eq(&self, other: &&str) -> bool {
450 match self {
451 ArchiveFileSpec::Glob(s) => s.as_str() == *other,
452 _ => false,
453 }
454 }
455}
456
457#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
461#[serde(default, deny_unknown_fields)]
462pub struct FileInfo {
463 pub owner: Option<String>,
465 pub group: Option<String>,
467 pub mode: Option<StringOrU32>,
472 pub mtime: Option<String>,
474}
475
476pub type ArchiveFileInfo = FileInfo;
478
479pub fn parse_octal_mode(s: &str) -> Option<u32> {
482 let cleaned = s
483 .strip_prefix("0o")
484 .or_else(|| s.strip_prefix("0O"))
485 .unwrap_or(s);
486 let cleaned = if cleaned.is_empty() { "0" } else { cleaned };
487 u32::from_str_radix(cleaned, 8).ok()
488}
489
490pub const VALID_ARCHIVE_FORMATS: &[&str] = &[
494 "tar.gz",
495 "tgz",
496 "tar.xz",
497 "txz",
498 "tar.zst",
499 "tzst",
500 "tar",
501 "zip",
502 "gz",
503 "xz",
504 crate::artifact::FORMAT_BINARY,
505 "none",
506];
507
508#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
517#[serde(untagged)]
518pub enum ExtraFileSpec {
519 Glob(String),
520 Detailed {
521 glob: String,
522 #[serde(default)]
524 name_template: Option<String>,
525 #[serde(default)]
531 allow_empty: bool,
532 },
533}
534
535impl ExtraFileSpec {
536 pub fn glob(&self) -> &str {
538 match self {
539 ExtraFileSpec::Glob(s) => s,
540 ExtraFileSpec::Detailed { glob, .. } => glob,
541 }
542 }
543
544 pub fn name_template(&self) -> Option<&str> {
546 match self {
547 ExtraFileSpec::Glob(_) => None,
548 ExtraFileSpec::Detailed { name_template, .. } => name_template.as_deref(),
549 }
550 }
551
552 pub fn allow_empty(&self) -> bool {
555 match self {
556 ExtraFileSpec::Glob(_) => false,
557 ExtraFileSpec::Detailed { allow_empty, .. } => *allow_empty,
558 }
559 }
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
565#[serde(default, deny_unknown_fields)]
566pub struct TemplatedExtraFile {
567 pub src: String,
569 pub dst: Option<String>,
572 pub mode: Option<String>,
575}
576
577#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
579#[serde(rename_all = "lowercase")]
580pub enum ChecksumSplitFormat {
581 #[default]
584 Bare,
585 Coreutils,
589}
590
591#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
592#[serde(default, deny_unknown_fields)]
593pub struct ChecksumConfig {
594 pub name_template: Option<String>,
596 pub algorithm: Option<String>,
602 #[serde(
605 alias = "disable",
606 deserialize_with = "deserialize_string_or_bool_opt",
607 default
608 )]
609 pub skip: Option<StringOrBool>,
610 pub extra_files: Option<Vec<ExtraFileSpec>>,
612 pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
615 pub ids: Option<Vec<String>>,
617 pub split: Option<bool>,
619 pub split_format: Option<ChecksumSplitFormat>,
624}
625
626impl ChecksumConfig {
627 pub const DEFAULT_NAME_TEMPLATE: &'static str = "{{ ProjectName }}_{{ Version }}_checksums.txt";
630
631 pub const DEFAULT_ALGORITHM: &'static str = "sha256";
633
634 pub const SUPPORTED_ALGORITHMS: &'static [&'static str] = &[
640 "sha1", "sha224", "sha256", "sha384", "sha512", "sha3-224", "sha3-256", "sha3-384",
641 "sha3-512", "blake2b", "blake2s", "blake3", "crc32", "md5",
642 ];
643
644 pub fn resolved_algorithm(&self) -> &str {
649 self.algorithm.as_deref().unwrap_or(Self::DEFAULT_ALGORITHM)
650 }
651
652 pub fn resolved_split(&self) -> bool {
655 self.split.unwrap_or(false)
656 }
657
658 pub fn resolved_combined_name_template(&self) -> &str {
666 self.name_template
667 .as_deref()
668 .unwrap_or(Self::DEFAULT_NAME_TEMPLATE)
669 }
670
671 pub fn resolve_combined_name_template<'a>(
679 crate_checksum: Option<&'a ChecksumConfig>,
680 global_checksum: Option<&'a ChecksumConfig>,
681 ) -> &'a str {
682 crate_checksum
683 .and_then(|c| c.name_template.as_deref())
684 .or_else(|| global_checksum.and_then(|c| c.name_template.as_deref()))
685 .unwrap_or(Self::DEFAULT_NAME_TEMPLATE)
686 }
687}
688
689#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
714#[serde(untagged)]
715pub enum ContentSource {
716 Inline(String),
717 FromFile {
718 from_file: String,
719 },
720 FromUrl {
721 from_url: String,
722 #[serde(default, skip_serializing_if = "Option::is_none")]
725 headers: Option<HashMap<String, String>>,
726 },
727}
728
729impl PartialEq for ContentSource {
730 fn eq(&self, other: &Self) -> bool {
731 match (self, other) {
732 (Self::Inline(a), Self::Inline(b)) => a == b,
733 (Self::FromFile { from_file: a }, Self::FromFile { from_file: b }) => a == b,
734 (
735 Self::FromUrl {
736 from_url: a,
737 headers: ha,
738 },
739 Self::FromUrl {
740 from_url: b,
741 headers: hb,
742 },
743 ) => a == b && ha == hb,
744 _ => false,
745 }
746 }
747}
748
749#[cfg(test)]
750mod tests {
751 use super::*;
752
753 #[derive(Deserialize)]
759 struct ArchivesWrapper {
760 #[serde(default, deserialize_with = "deserialize_archives_config")]
761 archives: ArchivesConfig,
762 }
763
764 #[test]
765 fn archives_false_is_disabled() {
766 let w: ArchivesWrapper = serde_yaml_ng::from_str("archives: false").unwrap();
767 assert!(matches!(w.archives, ArchivesConfig::Disabled));
768 }
769
770 #[test]
771 fn archives_true_is_rejected() {
772 let r: Result<ArchivesWrapper, _> = serde_yaml_ng::from_str("archives: true");
774 assert!(r.is_err(), "archives: true must be rejected");
775 }
776
777 #[test]
778 fn archives_list_becomes_configs() {
779 let w: ArchivesWrapper =
780 serde_yaml_ng::from_str("archives:\n - id: a\n - id: b\n").unwrap();
781 match w.archives {
782 ArchivesConfig::Configs(c) => assert_eq!(c.len(), 2),
783 other => panic!("expected Configs, got {other:?}"),
784 }
785 }
786
787 #[test]
788 fn archives_null_defaults_to_empty_configs() {
789 let w: ArchivesWrapper = serde_yaml_ng::from_str("archives: null").unwrap();
790 match w.archives {
791 ArchivesConfig::Configs(c) => assert!(c.is_empty()),
792 other => panic!("expected empty Configs, got {other:?}"),
793 }
794 }
795
796 #[derive(Deserialize)]
797 struct SignsWrapper {
798 #[serde(default, deserialize_with = "deserialize_signs")]
799 signs: Vec<SignConfig>,
800 }
801
802 #[test]
803 fn signs_single_object_becomes_one_element_vec() {
804 let w: SignsWrapper = serde_yaml_ng::from_str("signs:\n artifacts: all\n").unwrap();
806 assert_eq!(w.signs.len(), 1);
807 assert_eq!(w.signs[0].artifacts.as_deref(), Some("all"));
808 }
809
810 #[test]
811 fn signs_sequence_collects_all() {
812 let w: SignsWrapper =
813 serde_yaml_ng::from_str("signs:\n - artifacts: all\n - artifacts: checksum\n")
814 .unwrap();
815 assert_eq!(w.signs.len(), 2);
816 }
817
818 #[test]
819 fn signs_null_is_empty_vec() {
820 let w: SignsWrapper = serde_yaml_ng::from_str("signs: null").unwrap();
821 assert!(w.signs.is_empty());
822 }
823
824 #[derive(Deserialize)]
825 struct BinarySignsWrapper {
826 #[serde(default, deserialize_with = "deserialize_binary_signs")]
827 binary_signs: Vec<SignConfig>,
828 }
829
830 #[test]
831 fn binary_signs_accepts_binary_and_none() {
832 let w: BinarySignsWrapper =
833 serde_yaml_ng::from_str("binary_signs:\n - artifacts: binary\n").unwrap();
834 assert_eq!(w.binary_signs.len(), 1);
835 let w2: BinarySignsWrapper =
836 serde_yaml_ng::from_str("binary_signs:\n - artifacts: none\n").unwrap();
837 assert_eq!(w2.binary_signs.len(), 1);
838 }
839
840 #[test]
841 fn binary_signs_rejects_broad_artifact_filter() {
842 let r: Result<BinarySignsWrapper, _> =
844 serde_yaml_ng::from_str("binary_signs:\n - artifacts: all\n");
845 assert!(
846 r.is_err(),
847 "binary_signs must reject a non-binary artifact filter"
848 );
849 }
850
851 #[test]
854 fn wrap_in_directory_bool_true_uses_default_name() {
855 assert_eq!(
856 WrapInDirectory::Bool(true).directory_name("myapp_1.0"),
857 Some("myapp_1.0".to_string())
858 );
859 }
860
861 #[test]
862 fn wrap_in_directory_bool_false_disables_wrapping() {
863 assert_eq!(
864 WrapInDirectory::Bool(false).directory_name("myapp_1.0"),
865 None
866 );
867 }
868
869 #[test]
870 fn wrap_in_directory_empty_string_disables_wrapping() {
871 assert_eq!(
873 WrapInDirectory::Name(String::new()).directory_name("fallback"),
874 None
875 );
876 }
877
878 #[test]
879 fn wrap_in_directory_custom_name_overrides_default() {
880 assert_eq!(
881 WrapInDirectory::Name("custom".into()).directory_name("fallback"),
882 Some("custom".to_string())
883 );
884 }
885
886 #[derive(Deserialize)]
887 struct WrapWrapper {
888 wrap_in_directory: WrapInDirectory,
889 }
890
891 #[test]
892 fn wrap_in_directory_deserializes_bool_and_string() {
893 let b: WrapWrapper = serde_yaml_ng::from_str("wrap_in_directory: true").unwrap();
894 assert_eq!(b.wrap_in_directory, WrapInDirectory::Bool(true));
895 let s: WrapWrapper = serde_yaml_ng::from_str("wrap_in_directory: dist").unwrap();
896 assert_eq!(s.wrap_in_directory, WrapInDirectory::Name("dist".into()));
897 }
898
899 #[test]
900 fn wrap_in_directory_rejects_non_scalar() {
901 let r: Result<WrapWrapper, _> = serde_yaml_ng::from_str("wrap_in_directory:\n - a\n");
904 assert!(r.is_err());
905 }
906
907 #[test]
910 fn parse_octal_mode_accepts_common_forms() {
911 assert_eq!(parse_octal_mode("0755"), Some(0o755));
912 assert_eq!(parse_octal_mode("0o755"), Some(0o755));
913 assert_eq!(parse_octal_mode("0O755"), Some(0o755));
914 assert_eq!(parse_octal_mode("755"), Some(0o755));
915 assert_eq!(parse_octal_mode("0o"), Some(0));
918 assert_eq!(parse_octal_mode("0"), Some(0));
919 }
920
921 #[test]
922 fn parse_octal_mode_rejects_non_octal() {
923 assert_eq!(parse_octal_mode("0o899"), None);
925 assert_eq!(parse_octal_mode("garbage"), None);
926 }
927
928 #[test]
931 fn resolve_combined_name_template_prefers_crate_then_global_then_default() {
932 let crate_cfg = ChecksumConfig {
933 name_template: Some("crate.txt".into()),
934 ..Default::default()
935 };
936 let global_cfg = ChecksumConfig {
937 name_template: Some("global.txt".into()),
938 ..Default::default()
939 };
940 assert_eq!(
942 ChecksumConfig::resolve_combined_name_template(Some(&crate_cfg), Some(&global_cfg)),
943 "crate.txt"
944 );
945 let bare = ChecksumConfig::default();
947 assert_eq!(
948 ChecksumConfig::resolve_combined_name_template(Some(&bare), Some(&global_cfg)),
949 "global.txt"
950 );
951 assert_eq!(
953 ChecksumConfig::resolve_combined_name_template(None, None),
954 ChecksumConfig::DEFAULT_NAME_TEMPLATE
955 );
956 }
957
958 #[test]
961 fn checksum_split_format_defaults_to_bare() {
962 assert_eq!(ChecksumSplitFormat::default(), ChecksumSplitFormat::Bare);
963 }
964
965 #[test]
966 fn checksum_split_format_deserializes_lowercase() {
967 assert_eq!(
968 serde_yaml_ng::from_str::<ChecksumSplitFormat>("bare").unwrap(),
969 ChecksumSplitFormat::Bare
970 );
971 assert_eq!(
972 serde_yaml_ng::from_str::<ChecksumSplitFormat>("coreutils").unwrap(),
973 ChecksumSplitFormat::Coreutils
974 );
975 assert!(serde_yaml_ng::from_str::<ChecksumSplitFormat>("Coreutils").is_err());
976 }
977
978 #[test]
981 fn extra_file_spec_allow_empty_only_true_for_detailed_opt_in() {
982 let bare: ExtraFileSpec = serde_yaml_ng::from_str("dist/*.sig").unwrap();
984 assert!(!bare.allow_empty());
985 let opt_in: ExtraFileSpec =
987 serde_yaml_ng::from_str("glob: keys/*.pub\nallow_empty: true").unwrap();
988 assert!(opt_in.allow_empty());
989 assert_eq!(opt_in.glob(), "keys/*.pub");
990 let default_off: ExtraFileSpec = serde_yaml_ng::from_str("glob: docs/*.pdf").unwrap();
992 assert!(!default_off.allow_empty());
993 }
994
995 #[test]
998 fn archive_file_spec_str_eq_matches_glob_only() {
999 assert!(ArchiveFileSpec::Glob("README.md".into()) == "README.md");
1000 assert!(ArchiveFileSpec::Glob("README.md".into()) != "other");
1001 let detailed = ArchiveFileSpec::Detailed {
1003 src: "README.md".into(),
1004 dst: None,
1005 info: None,
1006 strip_parent: None,
1007 };
1008 assert!(detailed != "README.md");
1009 }
1010
1011 #[test]
1014 fn archive_config_folds_singular_format_into_formats() {
1015 let c: ArchiveConfig = serde_yaml_ng::from_str("format: tar.gz").unwrap();
1017 assert_eq!(c.formats.as_deref().unwrap(), ["tar.gz"]);
1018 }
1019
1020 #[test]
1021 fn archive_config_folds_deprecated_builds_into_ids() {
1022 let c: ArchiveConfig = serde_yaml_ng::from_str("ids: [keep]\nbuilds: [legacy]").unwrap();
1023 let ids = c.ids.unwrap();
1024 assert!(ids.contains(&"keep".to_string()));
1025 assert!(ids.contains(&"legacy".to_string()));
1026 }
1027
1028 #[test]
1029 fn archive_config_defaults_id_to_default() {
1030 let c: ArchiveConfig = serde_yaml_ng::from_str("name_template: x").unwrap();
1033 assert_eq!(c.id.as_deref(), Some("default"));
1034 let named: ArchiveConfig = serde_yaml_ng::from_str("id: bins").unwrap();
1036 assert_eq!(named.id.as_deref(), Some("bins"));
1037 }
1038
1039 #[test]
1040 fn format_override_folds_singular_format() {
1041 let o: FormatOverride = serde_yaml_ng::from_str("os: windows\nformat: zip").unwrap();
1042 assert_eq!(o.os, "windows");
1043 assert_eq!(o.formats.as_deref().unwrap(), ["zip"]);
1044 }
1045
1046 #[test]
1049 fn content_source_partial_eq_by_variant_and_payload() {
1050 assert_eq!(
1051 ContentSource::Inline("a".into()),
1052 ContentSource::Inline("a".into())
1053 );
1054 assert_ne!(
1055 ContentSource::Inline("a".into()),
1056 ContentSource::Inline("b".into())
1057 );
1058 assert_ne!(
1060 ContentSource::Inline("a".into()),
1061 ContentSource::FromFile {
1062 from_file: "a".into()
1063 }
1064 );
1065 let mut h = HashMap::new();
1067 h.insert("Accept".to_string(), "text/plain".to_string());
1068 let with_headers = ContentSource::FromUrl {
1069 from_url: "u".into(),
1070 headers: Some(h.clone()),
1071 };
1072 assert_eq!(
1073 with_headers,
1074 ContentSource::FromUrl {
1075 from_url: "u".into(),
1076 headers: Some(h),
1077 }
1078 );
1079 assert_ne!(
1080 with_headers,
1081 ContentSource::FromUrl {
1082 from_url: "u".into(),
1083 headers: None,
1084 }
1085 );
1086 }
1087
1088 #[test]
1089 fn content_source_from_url_deserializes_headers() {
1090 let cs: ContentSource = serde_yaml_ng::from_str(
1091 "from_url: https://example.com/h.md\nheaders:\n X-Token: abc\n",
1092 )
1093 .unwrap();
1094 match cs {
1095 ContentSource::FromUrl { from_url, headers } => {
1096 assert_eq!(from_url, "https://example.com/h.md");
1097 assert_eq!(headers.unwrap().get("X-Token").unwrap(), "abc");
1098 }
1099 other => panic!("expected FromUrl, got {other:?}"),
1100 }
1101 }
1102
1103 #[test]
1106 fn templated_extra_file_parses_and_defaults() {
1107 let full: TemplatedExtraFile = serde_yaml_ng::from_str(
1108 "src: NOTES.tera\ndst: \"{{ ProjectName }}-NOTES.txt\"\nmode: \"0644\"",
1109 )
1110 .unwrap();
1111 assert_eq!(full.src, "NOTES.tera");
1112 assert_eq!(full.dst.as_deref(), Some("{{ ProjectName }}-NOTES.txt"));
1113 assert_eq!(full.mode.as_deref(), Some("0644"));
1114 let minimal: TemplatedExtraFile = serde_yaml_ng::from_str("src: NOTES.tera").unwrap();
1116 assert!(minimal.dst.is_none());
1117 assert!(minimal.mode.is_none());
1118 assert!(serde_yaml_ng::from_str::<TemplatedExtraFile>("src: x\nbogus: y").is_err());
1120 }
1121}