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)]
698#[serde(untagged)]
699pub enum ContentSource {
700 Inline(String),
701 FromFile {
702 from_file: String,
703 },
704 FromUrl {
705 from_url: String,
706 #[serde(default, skip_serializing_if = "Option::is_none")]
709 headers: Option<HashMap<String, String>>,
710 },
711}
712
713impl PartialEq for ContentSource {
714 fn eq(&self, other: &Self) -> bool {
715 match (self, other) {
716 (Self::Inline(a), Self::Inline(b)) => a == b,
717 (Self::FromFile { from_file: a }, Self::FromFile { from_file: b }) => a == b,
718 (
719 Self::FromUrl {
720 from_url: a,
721 headers: ha,
722 },
723 Self::FromUrl {
724 from_url: b,
725 headers: hb,
726 },
727 ) => a == b && ha == hb,
728 _ => false,
729 }
730 }
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736
737 #[derive(Deserialize)]
743 struct ArchivesWrapper {
744 #[serde(default, deserialize_with = "deserialize_archives_config")]
745 archives: ArchivesConfig,
746 }
747
748 #[test]
749 fn archives_false_is_disabled() {
750 let w: ArchivesWrapper = serde_yaml_ng::from_str("archives: false").unwrap();
751 assert!(matches!(w.archives, ArchivesConfig::Disabled));
752 }
753
754 #[test]
755 fn archives_true_is_rejected() {
756 let r: Result<ArchivesWrapper, _> = serde_yaml_ng::from_str("archives: true");
758 assert!(r.is_err(), "archives: true must be rejected");
759 }
760
761 #[test]
762 fn archives_list_becomes_configs() {
763 let w: ArchivesWrapper =
764 serde_yaml_ng::from_str("archives:\n - id: a\n - id: b\n").unwrap();
765 match w.archives {
766 ArchivesConfig::Configs(c) => assert_eq!(c.len(), 2),
767 other => panic!("expected Configs, got {other:?}"),
768 }
769 }
770
771 #[test]
772 fn archives_null_defaults_to_empty_configs() {
773 let w: ArchivesWrapper = serde_yaml_ng::from_str("archives: null").unwrap();
774 match w.archives {
775 ArchivesConfig::Configs(c) => assert!(c.is_empty()),
776 other => panic!("expected empty Configs, got {other:?}"),
777 }
778 }
779
780 #[derive(Deserialize)]
781 struct SignsWrapper {
782 #[serde(default, deserialize_with = "deserialize_signs")]
783 signs: Vec<SignConfig>,
784 }
785
786 #[test]
787 fn signs_single_object_becomes_one_element_vec() {
788 let w: SignsWrapper = serde_yaml_ng::from_str("signs:\n artifacts: all\n").unwrap();
790 assert_eq!(w.signs.len(), 1);
791 assert_eq!(w.signs[0].artifacts.as_deref(), Some("all"));
792 }
793
794 #[test]
795 fn signs_sequence_collects_all() {
796 let w: SignsWrapper =
797 serde_yaml_ng::from_str("signs:\n - artifacts: all\n - artifacts: checksum\n")
798 .unwrap();
799 assert_eq!(w.signs.len(), 2);
800 }
801
802 #[test]
803 fn signs_null_is_empty_vec() {
804 let w: SignsWrapper = serde_yaml_ng::from_str("signs: null").unwrap();
805 assert!(w.signs.is_empty());
806 }
807
808 #[derive(Deserialize)]
809 struct BinarySignsWrapper {
810 #[serde(default, deserialize_with = "deserialize_binary_signs")]
811 binary_signs: Vec<SignConfig>,
812 }
813
814 #[test]
815 fn binary_signs_accepts_binary_and_none() {
816 let w: BinarySignsWrapper =
817 serde_yaml_ng::from_str("binary_signs:\n - artifacts: binary\n").unwrap();
818 assert_eq!(w.binary_signs.len(), 1);
819 let w2: BinarySignsWrapper =
820 serde_yaml_ng::from_str("binary_signs:\n - artifacts: none\n").unwrap();
821 assert_eq!(w2.binary_signs.len(), 1);
822 }
823
824 #[test]
825 fn binary_signs_rejects_broad_artifact_filter() {
826 let r: Result<BinarySignsWrapper, _> =
828 serde_yaml_ng::from_str("binary_signs:\n - artifacts: all\n");
829 assert!(
830 r.is_err(),
831 "binary_signs must reject a non-binary artifact filter"
832 );
833 }
834
835 #[test]
838 fn wrap_in_directory_bool_true_uses_default_name() {
839 assert_eq!(
840 WrapInDirectory::Bool(true).directory_name("myapp_1.0"),
841 Some("myapp_1.0".to_string())
842 );
843 }
844
845 #[test]
846 fn wrap_in_directory_bool_false_disables_wrapping() {
847 assert_eq!(
848 WrapInDirectory::Bool(false).directory_name("myapp_1.0"),
849 None
850 );
851 }
852
853 #[test]
854 fn wrap_in_directory_empty_string_disables_wrapping() {
855 assert_eq!(
857 WrapInDirectory::Name(String::new()).directory_name("fallback"),
858 None
859 );
860 }
861
862 #[test]
863 fn wrap_in_directory_custom_name_overrides_default() {
864 assert_eq!(
865 WrapInDirectory::Name("custom".into()).directory_name("fallback"),
866 Some("custom".to_string())
867 );
868 }
869
870 #[derive(Deserialize)]
871 struct WrapWrapper {
872 wrap_in_directory: WrapInDirectory,
873 }
874
875 #[test]
876 fn wrap_in_directory_deserializes_bool_and_string() {
877 let b: WrapWrapper = serde_yaml_ng::from_str("wrap_in_directory: true").unwrap();
878 assert_eq!(b.wrap_in_directory, WrapInDirectory::Bool(true));
879 let s: WrapWrapper = serde_yaml_ng::from_str("wrap_in_directory: dist").unwrap();
880 assert_eq!(s.wrap_in_directory, WrapInDirectory::Name("dist".into()));
881 }
882
883 #[test]
884 fn wrap_in_directory_rejects_non_scalar() {
885 let r: Result<WrapWrapper, _> = serde_yaml_ng::from_str("wrap_in_directory:\n - a\n");
888 assert!(r.is_err());
889 }
890
891 #[test]
894 fn parse_octal_mode_accepts_common_forms() {
895 assert_eq!(parse_octal_mode("0755"), Some(0o755));
896 assert_eq!(parse_octal_mode("0o755"), Some(0o755));
897 assert_eq!(parse_octal_mode("0O755"), Some(0o755));
898 assert_eq!(parse_octal_mode("755"), Some(0o755));
899 assert_eq!(parse_octal_mode("0o"), Some(0));
902 assert_eq!(parse_octal_mode("0"), Some(0));
903 }
904
905 #[test]
906 fn parse_octal_mode_rejects_non_octal() {
907 assert_eq!(parse_octal_mode("0o899"), None);
909 assert_eq!(parse_octal_mode("garbage"), None);
910 }
911
912 #[test]
915 fn resolve_combined_name_template_prefers_crate_then_global_then_default() {
916 let crate_cfg = ChecksumConfig {
917 name_template: Some("crate.txt".into()),
918 ..Default::default()
919 };
920 let global_cfg = ChecksumConfig {
921 name_template: Some("global.txt".into()),
922 ..Default::default()
923 };
924 assert_eq!(
926 ChecksumConfig::resolve_combined_name_template(Some(&crate_cfg), Some(&global_cfg)),
927 "crate.txt"
928 );
929 let bare = ChecksumConfig::default();
931 assert_eq!(
932 ChecksumConfig::resolve_combined_name_template(Some(&bare), Some(&global_cfg)),
933 "global.txt"
934 );
935 assert_eq!(
937 ChecksumConfig::resolve_combined_name_template(None, None),
938 ChecksumConfig::DEFAULT_NAME_TEMPLATE
939 );
940 }
941
942 #[test]
945 fn checksum_split_format_defaults_to_bare() {
946 assert_eq!(ChecksumSplitFormat::default(), ChecksumSplitFormat::Bare);
947 }
948
949 #[test]
950 fn checksum_split_format_deserializes_lowercase() {
951 assert_eq!(
952 serde_yaml_ng::from_str::<ChecksumSplitFormat>("bare").unwrap(),
953 ChecksumSplitFormat::Bare
954 );
955 assert_eq!(
956 serde_yaml_ng::from_str::<ChecksumSplitFormat>("coreutils").unwrap(),
957 ChecksumSplitFormat::Coreutils
958 );
959 assert!(serde_yaml_ng::from_str::<ChecksumSplitFormat>("Coreutils").is_err());
960 }
961
962 #[test]
965 fn extra_file_spec_allow_empty_only_true_for_detailed_opt_in() {
966 let bare: ExtraFileSpec = serde_yaml_ng::from_str("dist/*.sig").unwrap();
968 assert!(!bare.allow_empty());
969 let opt_in: ExtraFileSpec =
971 serde_yaml_ng::from_str("glob: keys/*.pub\nallow_empty: true").unwrap();
972 assert!(opt_in.allow_empty());
973 assert_eq!(opt_in.glob(), "keys/*.pub");
974 let default_off: ExtraFileSpec = serde_yaml_ng::from_str("glob: docs/*.pdf").unwrap();
976 assert!(!default_off.allow_empty());
977 }
978
979 #[test]
982 fn archive_file_spec_str_eq_matches_glob_only() {
983 assert!(ArchiveFileSpec::Glob("README.md".into()) == "README.md");
984 assert!(ArchiveFileSpec::Glob("README.md".into()) != "other");
985 let detailed = ArchiveFileSpec::Detailed {
987 src: "README.md".into(),
988 dst: None,
989 info: None,
990 strip_parent: None,
991 };
992 assert!(detailed != "README.md");
993 }
994
995 #[test]
998 fn archive_config_folds_singular_format_into_formats() {
999 let c: ArchiveConfig = serde_yaml_ng::from_str("format: tar.gz").unwrap();
1001 assert_eq!(c.formats.as_deref().unwrap(), ["tar.gz"]);
1002 }
1003
1004 #[test]
1005 fn archive_config_folds_deprecated_builds_into_ids() {
1006 let c: ArchiveConfig = serde_yaml_ng::from_str("ids: [keep]\nbuilds: [legacy]").unwrap();
1007 let ids = c.ids.unwrap();
1008 assert!(ids.contains(&"keep".to_string()));
1009 assert!(ids.contains(&"legacy".to_string()));
1010 }
1011
1012 #[test]
1013 fn archive_config_defaults_id_to_default() {
1014 let c: ArchiveConfig = serde_yaml_ng::from_str("name_template: x").unwrap();
1017 assert_eq!(c.id.as_deref(), Some("default"));
1018 let named: ArchiveConfig = serde_yaml_ng::from_str("id: bins").unwrap();
1020 assert_eq!(named.id.as_deref(), Some("bins"));
1021 }
1022
1023 #[test]
1024 fn format_override_folds_singular_format() {
1025 let o: FormatOverride = serde_yaml_ng::from_str("os: windows\nformat: zip").unwrap();
1026 assert_eq!(o.os, "windows");
1027 assert_eq!(o.formats.as_deref().unwrap(), ["zip"]);
1028 }
1029
1030 #[test]
1033 fn content_source_partial_eq_by_variant_and_payload() {
1034 assert_eq!(
1035 ContentSource::Inline("a".into()),
1036 ContentSource::Inline("a".into())
1037 );
1038 assert_ne!(
1039 ContentSource::Inline("a".into()),
1040 ContentSource::Inline("b".into())
1041 );
1042 assert_ne!(
1044 ContentSource::Inline("a".into()),
1045 ContentSource::FromFile {
1046 from_file: "a".into()
1047 }
1048 );
1049 let mut h = HashMap::new();
1051 h.insert("Accept".to_string(), "text/plain".to_string());
1052 let with_headers = ContentSource::FromUrl {
1053 from_url: "u".into(),
1054 headers: Some(h.clone()),
1055 };
1056 assert_eq!(
1057 with_headers,
1058 ContentSource::FromUrl {
1059 from_url: "u".into(),
1060 headers: Some(h),
1061 }
1062 );
1063 assert_ne!(
1064 with_headers,
1065 ContentSource::FromUrl {
1066 from_url: "u".into(),
1067 headers: None,
1068 }
1069 );
1070 }
1071
1072 #[test]
1073 fn content_source_from_url_deserializes_headers() {
1074 let cs: ContentSource = serde_yaml_ng::from_str(
1075 "from_url: https://example.com/h.md\nheaders:\n X-Token: abc\n",
1076 )
1077 .unwrap();
1078 match cs {
1079 ContentSource::FromUrl { from_url, headers } => {
1080 assert_eq!(from_url, "https://example.com/h.md");
1081 assert_eq!(headers.unwrap().get("X-Token").unwrap(), "abc");
1082 }
1083 other => panic!("expected FromUrl, got {other:?}"),
1084 }
1085 }
1086
1087 #[test]
1090 fn templated_extra_file_parses_and_defaults() {
1091 let full: TemplatedExtraFile = serde_yaml_ng::from_str(
1092 "src: NOTES.tera\ndst: \"{{ ProjectName }}-NOTES.txt\"\nmode: \"0644\"",
1093 )
1094 .unwrap();
1095 assert_eq!(full.src, "NOTES.tera");
1096 assert_eq!(full.dst.as_deref(), Some("{{ ProjectName }}-NOTES.txt"));
1097 assert_eq!(full.mode.as_deref(), Some("0644"));
1098 let minimal: TemplatedExtraFile = serde_yaml_ng::from_str("src: NOTES.tera").unwrap();
1100 assert!(minimal.dst.is_none());
1101 assert!(minimal.mode.is_none());
1102 assert!(serde_yaml_ng::from_str::<TemplatedExtraFile>("src: x\nbogus: y").is_err());
1104 }
1105}