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",
493 "tgz",
494 "tar.xz",
495 "txz",
496 "tar.zst",
497 "tzst",
498 "tar",
499 "zip",
500 "gz",
501 "xz",
502 crate::artifact::FORMAT_BINARY,
503 "none",
504];
505
506#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
515#[serde(untagged)]
516pub enum ExtraFileSpec {
517 Glob(String),
518 Detailed {
519 glob: String,
520 #[serde(default)]
522 name_template: Option<String>,
523 #[serde(default)]
529 allow_empty: bool,
530 },
531}
532
533impl ExtraFileSpec {
534 pub fn glob(&self) -> &str {
536 match self {
537 ExtraFileSpec::Glob(s) => s,
538 ExtraFileSpec::Detailed { glob, .. } => glob,
539 }
540 }
541
542 pub fn name_template(&self) -> Option<&str> {
544 match self {
545 ExtraFileSpec::Glob(_) => None,
546 ExtraFileSpec::Detailed { name_template, .. } => name_template.as_deref(),
547 }
548 }
549
550 pub fn allow_empty(&self) -> bool {
553 match self {
554 ExtraFileSpec::Glob(_) => false,
555 ExtraFileSpec::Detailed { allow_empty, .. } => *allow_empty,
556 }
557 }
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
563#[serde(default, deny_unknown_fields)]
564pub struct TemplatedExtraFile {
565 pub src: String,
567 pub dst: Option<String>,
570 pub mode: Option<String>,
573}
574
575#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
577#[serde(rename_all = "lowercase")]
578pub enum ChecksumSplitFormat {
579 #[default]
582 Bare,
583 Coreutils,
587}
588
589#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
590#[serde(default, deny_unknown_fields)]
591pub struct ChecksumConfig {
592 pub name_template: Option<String>,
594 pub algorithm: Option<String>,
600 #[serde(
603 alias = "disable",
604 deserialize_with = "deserialize_string_or_bool_opt",
605 default
606 )]
607 pub skip: Option<StringOrBool>,
608 pub extra_files: Option<Vec<ExtraFileSpec>>,
610 pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
613 pub ids: Option<Vec<String>>,
615 pub split: Option<bool>,
617 pub split_format: Option<ChecksumSplitFormat>,
622}
623
624impl ChecksumConfig {
625 pub const DEFAULT_NAME_TEMPLATE: &'static str = "{{ ProjectName }}_{{ Version }}_checksums.txt";
628
629 pub const DEFAULT_ALGORITHM: &'static str = "sha256";
631
632 pub const SUPPORTED_ALGORITHMS: &'static [&'static str] = &[
638 "sha1", "sha224", "sha256", "sha384", "sha512", "sha3-224", "sha3-256", "sha3-384",
639 "sha3-512", "blake2b", "blake2s", "blake3", "crc32", "md5",
640 ];
641
642 pub fn resolved_algorithm(&self) -> &str {
647 self.algorithm.as_deref().unwrap_or(Self::DEFAULT_ALGORITHM)
648 }
649
650 pub fn resolved_split(&self) -> bool {
653 self.split.unwrap_or(false)
654 }
655
656 pub fn resolved_combined_name_template(&self) -> &str {
664 self.name_template
665 .as_deref()
666 .unwrap_or(Self::DEFAULT_NAME_TEMPLATE)
667 }
668
669 pub fn resolve_combined_name_template<'a>(
677 crate_checksum: Option<&'a ChecksumConfig>,
678 global_checksum: Option<&'a ChecksumConfig>,
679 ) -> &'a str {
680 crate_checksum
681 .and_then(|c| c.name_template.as_deref())
682 .or_else(|| global_checksum.and_then(|c| c.name_template.as_deref()))
683 .unwrap_or(Self::DEFAULT_NAME_TEMPLATE)
684 }
685}
686
687#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
712#[serde(untagged)]
713pub enum ContentSource {
714 Inline(String),
715 FromFile {
716 from_file: String,
717 },
718 FromUrl {
719 from_url: String,
720 #[serde(default, skip_serializing_if = "Option::is_none")]
723 headers: Option<HashMap<String, String>>,
724 },
725}
726
727impl PartialEq for ContentSource {
728 fn eq(&self, other: &Self) -> bool {
729 match (self, other) {
730 (Self::Inline(a), Self::Inline(b)) => a == b,
731 (Self::FromFile { from_file: a }, Self::FromFile { from_file: b }) => a == b,
732 (
733 Self::FromUrl {
734 from_url: a,
735 headers: ha,
736 },
737 Self::FromUrl {
738 from_url: b,
739 headers: hb,
740 },
741 ) => a == b && ha == hb,
742 _ => false,
743 }
744 }
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750
751 #[derive(Deserialize)]
757 struct ArchivesWrapper {
758 #[serde(default, deserialize_with = "deserialize_archives_config")]
759 archives: ArchivesConfig,
760 }
761
762 #[test]
763 fn archives_false_is_disabled() {
764 let w: ArchivesWrapper = serde_yaml_ng::from_str("archives: false").unwrap();
765 assert!(matches!(w.archives, ArchivesConfig::Disabled));
766 }
767
768 #[test]
769 fn archives_true_is_rejected() {
770 let r: Result<ArchivesWrapper, _> = serde_yaml_ng::from_str("archives: true");
772 assert!(r.is_err(), "archives: true must be rejected");
773 }
774
775 #[test]
776 fn archives_list_becomes_configs() {
777 let w: ArchivesWrapper =
778 serde_yaml_ng::from_str("archives:\n - id: a\n - id: b\n").unwrap();
779 match w.archives {
780 ArchivesConfig::Configs(c) => assert_eq!(c.len(), 2),
781 other => panic!("expected Configs, got {other:?}"),
782 }
783 }
784
785 #[test]
786 fn archives_null_defaults_to_empty_configs() {
787 let w: ArchivesWrapper = serde_yaml_ng::from_str("archives: null").unwrap();
788 match w.archives {
789 ArchivesConfig::Configs(c) => assert!(c.is_empty()),
790 other => panic!("expected empty Configs, got {other:?}"),
791 }
792 }
793
794 #[derive(Deserialize)]
795 struct SignsWrapper {
796 #[serde(default, deserialize_with = "deserialize_signs")]
797 signs: Vec<SignConfig>,
798 }
799
800 #[test]
801 fn signs_single_object_becomes_one_element_vec() {
802 let w: SignsWrapper = serde_yaml_ng::from_str("signs:\n artifacts: all\n").unwrap();
804 assert_eq!(w.signs.len(), 1);
805 assert_eq!(w.signs[0].artifacts.as_deref(), Some("all"));
806 }
807
808 #[test]
809 fn signs_sequence_collects_all() {
810 let w: SignsWrapper =
811 serde_yaml_ng::from_str("signs:\n - artifacts: all\n - artifacts: checksum\n")
812 .unwrap();
813 assert_eq!(w.signs.len(), 2);
814 }
815
816 #[test]
817 fn signs_null_is_empty_vec() {
818 let w: SignsWrapper = serde_yaml_ng::from_str("signs: null").unwrap();
819 assert!(w.signs.is_empty());
820 }
821
822 #[derive(Deserialize)]
823 struct BinarySignsWrapper {
824 #[serde(default, deserialize_with = "deserialize_binary_signs")]
825 binary_signs: Vec<SignConfig>,
826 }
827
828 #[test]
829 fn binary_signs_accepts_binary_and_none() {
830 let w: BinarySignsWrapper =
831 serde_yaml_ng::from_str("binary_signs:\n - artifacts: binary\n").unwrap();
832 assert_eq!(w.binary_signs.len(), 1);
833 let w2: BinarySignsWrapper =
834 serde_yaml_ng::from_str("binary_signs:\n - artifacts: none\n").unwrap();
835 assert_eq!(w2.binary_signs.len(), 1);
836 }
837
838 #[test]
839 fn binary_signs_rejects_broad_artifact_filter() {
840 let r: Result<BinarySignsWrapper, _> =
842 serde_yaml_ng::from_str("binary_signs:\n - artifacts: all\n");
843 assert!(
844 r.is_err(),
845 "binary_signs must reject a non-binary artifact filter"
846 );
847 }
848
849 #[test]
852 fn wrap_in_directory_bool_true_uses_default_name() {
853 assert_eq!(
854 WrapInDirectory::Bool(true).directory_name("myapp_1.0"),
855 Some("myapp_1.0".to_string())
856 );
857 }
858
859 #[test]
860 fn wrap_in_directory_bool_false_disables_wrapping() {
861 assert_eq!(
862 WrapInDirectory::Bool(false).directory_name("myapp_1.0"),
863 None
864 );
865 }
866
867 #[test]
868 fn wrap_in_directory_empty_string_disables_wrapping() {
869 assert_eq!(
871 WrapInDirectory::Name(String::new()).directory_name("fallback"),
872 None
873 );
874 }
875
876 #[test]
877 fn wrap_in_directory_custom_name_overrides_default() {
878 assert_eq!(
879 WrapInDirectory::Name("custom".into()).directory_name("fallback"),
880 Some("custom".to_string())
881 );
882 }
883
884 #[derive(Deserialize)]
885 struct WrapWrapper {
886 wrap_in_directory: WrapInDirectory,
887 }
888
889 #[test]
890 fn wrap_in_directory_deserializes_bool_and_string() {
891 let b: WrapWrapper = serde_yaml_ng::from_str("wrap_in_directory: true").unwrap();
892 assert_eq!(b.wrap_in_directory, WrapInDirectory::Bool(true));
893 let s: WrapWrapper = serde_yaml_ng::from_str("wrap_in_directory: dist").unwrap();
894 assert_eq!(s.wrap_in_directory, WrapInDirectory::Name("dist".into()));
895 }
896
897 #[test]
898 fn wrap_in_directory_rejects_non_scalar() {
899 let r: Result<WrapWrapper, _> = serde_yaml_ng::from_str("wrap_in_directory:\n - a\n");
902 assert!(r.is_err());
903 }
904
905 #[test]
908 fn parse_octal_mode_accepts_common_forms() {
909 assert_eq!(parse_octal_mode("0755"), Some(0o755));
910 assert_eq!(parse_octal_mode("0o755"), Some(0o755));
911 assert_eq!(parse_octal_mode("0O755"), Some(0o755));
912 assert_eq!(parse_octal_mode("755"), Some(0o755));
913 assert_eq!(parse_octal_mode("0o"), Some(0));
916 assert_eq!(parse_octal_mode("0"), Some(0));
917 }
918
919 #[test]
920 fn parse_octal_mode_rejects_non_octal() {
921 assert_eq!(parse_octal_mode("0o899"), None);
923 assert_eq!(parse_octal_mode("garbage"), None);
924 }
925
926 #[test]
929 fn resolve_combined_name_template_prefers_crate_then_global_then_default() {
930 let crate_cfg = ChecksumConfig {
931 name_template: Some("crate.txt".into()),
932 ..Default::default()
933 };
934 let global_cfg = ChecksumConfig {
935 name_template: Some("global.txt".into()),
936 ..Default::default()
937 };
938 assert_eq!(
940 ChecksumConfig::resolve_combined_name_template(Some(&crate_cfg), Some(&global_cfg)),
941 "crate.txt"
942 );
943 let bare = ChecksumConfig::default();
945 assert_eq!(
946 ChecksumConfig::resolve_combined_name_template(Some(&bare), Some(&global_cfg)),
947 "global.txt"
948 );
949 assert_eq!(
951 ChecksumConfig::resolve_combined_name_template(None, None),
952 ChecksumConfig::DEFAULT_NAME_TEMPLATE
953 );
954 }
955
956 #[test]
959 fn checksum_split_format_defaults_to_bare() {
960 assert_eq!(ChecksumSplitFormat::default(), ChecksumSplitFormat::Bare);
961 }
962
963 #[test]
964 fn checksum_split_format_deserializes_lowercase() {
965 assert_eq!(
966 serde_yaml_ng::from_str::<ChecksumSplitFormat>("bare").unwrap(),
967 ChecksumSplitFormat::Bare
968 );
969 assert_eq!(
970 serde_yaml_ng::from_str::<ChecksumSplitFormat>("coreutils").unwrap(),
971 ChecksumSplitFormat::Coreutils
972 );
973 assert!(serde_yaml_ng::from_str::<ChecksumSplitFormat>("Coreutils").is_err());
974 }
975
976 #[test]
979 fn extra_file_spec_allow_empty_only_true_for_detailed_opt_in() {
980 let bare: ExtraFileSpec = serde_yaml_ng::from_str("dist/*.sig").unwrap();
982 assert!(!bare.allow_empty());
983 let opt_in: ExtraFileSpec =
985 serde_yaml_ng::from_str("glob: keys/*.pub\nallow_empty: true").unwrap();
986 assert!(opt_in.allow_empty());
987 assert_eq!(opt_in.glob(), "keys/*.pub");
988 let default_off: ExtraFileSpec = serde_yaml_ng::from_str("glob: docs/*.pdf").unwrap();
990 assert!(!default_off.allow_empty());
991 }
992
993 #[test]
996 fn archive_file_spec_str_eq_matches_glob_only() {
997 assert!(ArchiveFileSpec::Glob("README.md".into()) == "README.md");
998 assert!(ArchiveFileSpec::Glob("README.md".into()) != "other");
999 let detailed = ArchiveFileSpec::Detailed {
1001 src: "README.md".into(),
1002 dst: None,
1003 info: None,
1004 strip_parent: None,
1005 };
1006 assert!(detailed != "README.md");
1007 }
1008
1009 #[test]
1012 fn archive_config_folds_singular_format_into_formats() {
1013 let c: ArchiveConfig = serde_yaml_ng::from_str("format: tar.gz").unwrap();
1015 assert_eq!(c.formats.as_deref().unwrap(), ["tar.gz"]);
1016 }
1017
1018 #[test]
1019 fn archive_config_folds_deprecated_builds_into_ids() {
1020 let c: ArchiveConfig = serde_yaml_ng::from_str("ids: [keep]\nbuilds: [legacy]").unwrap();
1021 let ids = c.ids.unwrap();
1022 assert!(ids.contains(&"keep".to_string()));
1023 assert!(ids.contains(&"legacy".to_string()));
1024 }
1025
1026 #[test]
1027 fn archive_config_defaults_id_to_default() {
1028 let c: ArchiveConfig = serde_yaml_ng::from_str("name_template: x").unwrap();
1031 assert_eq!(c.id.as_deref(), Some("default"));
1032 let named: ArchiveConfig = serde_yaml_ng::from_str("id: bins").unwrap();
1034 assert_eq!(named.id.as_deref(), Some("bins"));
1035 }
1036
1037 #[test]
1038 fn format_override_folds_singular_format() {
1039 let o: FormatOverride = serde_yaml_ng::from_str("os: windows\nformat: zip").unwrap();
1040 assert_eq!(o.os, "windows");
1041 assert_eq!(o.formats.as_deref().unwrap(), ["zip"]);
1042 }
1043
1044 #[test]
1047 fn content_source_partial_eq_by_variant_and_payload() {
1048 assert_eq!(
1049 ContentSource::Inline("a".into()),
1050 ContentSource::Inline("a".into())
1051 );
1052 assert_ne!(
1053 ContentSource::Inline("a".into()),
1054 ContentSource::Inline("b".into())
1055 );
1056 assert_ne!(
1058 ContentSource::Inline("a".into()),
1059 ContentSource::FromFile {
1060 from_file: "a".into()
1061 }
1062 );
1063 let mut h = HashMap::new();
1065 h.insert("Accept".to_string(), "text/plain".to_string());
1066 let with_headers = ContentSource::FromUrl {
1067 from_url: "u".into(),
1068 headers: Some(h.clone()),
1069 };
1070 assert_eq!(
1071 with_headers,
1072 ContentSource::FromUrl {
1073 from_url: "u".into(),
1074 headers: Some(h),
1075 }
1076 );
1077 assert_ne!(
1078 with_headers,
1079 ContentSource::FromUrl {
1080 from_url: "u".into(),
1081 headers: None,
1082 }
1083 );
1084 }
1085
1086 #[test]
1087 fn content_source_from_url_deserializes_headers() {
1088 let cs: ContentSource = serde_yaml_ng::from_str(
1089 "from_url: https://example.com/h.md\nheaders:\n X-Token: abc\n",
1090 )
1091 .unwrap();
1092 match cs {
1093 ContentSource::FromUrl { from_url, headers } => {
1094 assert_eq!(from_url, "https://example.com/h.md");
1095 assert_eq!(headers.unwrap().get("X-Token").unwrap(), "abc");
1096 }
1097 other => panic!("expected FromUrl, got {other:?}"),
1098 }
1099 }
1100
1101 #[test]
1104 fn templated_extra_file_parses_and_defaults() {
1105 let full: TemplatedExtraFile = serde_yaml_ng::from_str(
1106 "src: NOTES.tera\ndst: \"{{ ProjectName }}-NOTES.txt\"\nmode: \"0644\"",
1107 )
1108 .unwrap();
1109 assert_eq!(full.src, "NOTES.tera");
1110 assert_eq!(full.dst.as_deref(), Some("{{ ProjectName }}-NOTES.txt"));
1111 assert_eq!(full.mode.as_deref(), Some("0644"));
1112 let minimal: TemplatedExtraFile = serde_yaml_ng::from_str("src: NOTES.tera").unwrap();
1114 assert!(minimal.dst.is_none());
1115 assert!(minimal.mode.is_none());
1116 assert!(serde_yaml_ng::from_str::<TemplatedExtraFile>("src: x\nbogus: y").is_err());
1118 }
1119}