1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
7
8pub const CPIO_EXTENSION: &str = "cpio";
10pub const CPIO_GZIP_EXTENSION: &str = "cpio.gz";
12pub const CPIO_XZ_EXTENSION: &str = "cpio.xz";
14pub const CPIO_ZSTD_EXTENSION: &str = "cpio.zst";
16pub const CPIO_EXTENSIONS: &[&str] = &["cpio", "cpio.gz", "cpio.xz", "cpio.zst"];
18
19#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
21pub enum CpioFormat {
22 Binary,
24 OldAscii,
26 NewAscii,
28 CrcAscii,
30 #[default]
32 Unknown,
33}
34
35impl CpioFormat {
36 #[must_use]
38 pub const fn as_str(self) -> &'static str {
39 match self {
40 Self::Binary => "binary",
41 Self::OldAscii => "old-ascii",
42 Self::NewAscii => "new-ascii",
43 Self::CrcAscii => "crc-ascii",
44 Self::Unknown => "unknown",
45 }
46 }
47}
48
49impl fmt::Display for CpioFormat {
50 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51 formatter.write_str(self.as_str())
52 }
53}
54
55#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
57pub enum CpioEntryKind {
58 File,
60 Directory,
62 Symlink,
64 Device,
66 Fifo,
68 Socket,
70 #[default]
72 Unknown,
73}
74
75impl CpioEntryKind {
76 #[must_use]
78 pub const fn as_str(self) -> &'static str {
79 match self {
80 Self::File => "file",
81 Self::Directory => "directory",
82 Self::Symlink => "symlink",
83 Self::Device => "device",
84 Self::Fifo => "fifo",
85 Self::Socket => "socket",
86 Self::Unknown => "unknown",
87 }
88 }
89}
90
91impl fmt::Display for CpioEntryKind {
92 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93 formatter.write_str(self.as_str())
94 }
95}
96
97#[must_use]
99pub fn is_cpio_extension(extension: &str) -> bool {
100 matches!(
101 normalize_extension(extension).as_str(),
102 "cpio" | "cpio.gz" | "cpio.xz" | "cpio.zst" | "cpio.zstd"
103 )
104}
105
106#[must_use]
108pub fn is_cpio_filename(name: &str) -> bool {
109 let parts = filename_parts(name);
110
111 match parts.as_slice() {
112 [.., last] if last == "cpio" => true,
113 [.., previous, last]
114 if previous == "cpio" && matches!(last.as_str(), "gz" | "xz" | "zst" | "zstd") =>
115 {
116 true
117 },
118 _ => false,
119 }
120}
121
122fn normalize_extension(extension: &str) -> String {
123 extension
124 .trim()
125 .trim_start_matches('.')
126 .to_ascii_lowercase()
127}
128
129fn filename_parts(name: &str) -> Vec<String> {
130 name.trim()
131 .to_ascii_lowercase()
132 .rsplit(['/', '\\'])
133 .next()
134 .unwrap_or_default()
135 .trim_start_matches('.')
136 .split('.')
137 .filter(|part| !part.is_empty())
138 .map(str::to_owned)
139 .collect()
140}
141
142#[cfg(test)]
143mod tests {
144 use super::{CPIO_EXTENSIONS, CpioEntryKind, CpioFormat, is_cpio_extension, is_cpio_filename};
145
146 #[test]
147 fn detects_cpio_extensions() {
148 assert!(is_cpio_extension(".cpio"));
149 assert!(is_cpio_extension("cpio.gz"));
150 assert!(is_cpio_extension("cpio.zst"));
151 assert_eq!(CPIO_EXTENSIONS[0], "cpio");
152 }
153
154 #[test]
155 fn detects_cpio_filenames() {
156 assert!(is_cpio_filename("initramfs.cpio"));
157 assert!(is_cpio_filename("initramfs.CPIO.XZ"));
158 assert!(!is_cpio_filename("bundle.tar"));
159 }
160
161 #[test]
162 fn exposes_default_and_unknown_labels() {
163 assert_eq!(CpioFormat::default(), CpioFormat::Unknown);
164 assert_eq!(CpioFormat::CrcAscii.as_str(), "crc-ascii");
165 assert_eq!(CpioEntryKind::default(), CpioEntryKind::Unknown);
166 assert_eq!(CpioEntryKind::Socket.as_str(), "socket");
167 }
168}