conduit_cli/core/engine/
archive.rs1use crate::errors::{ConduitError, ConduitResult};
2use std::fs::File;
3use std::io::{Read, Write};
4use std::path::Path;
5use zip::write::FileOptions;
6use zip::{ZipArchive, ZipWriter};
7
8pub struct SafeArchive;
9
10impl SafeArchive {
11 pub fn open<P: AsRef<Path>>(path: P) -> ConduitResult<ZipArchive<File>> {
12 let file = File::open(&path)?;
13 ZipArchive::new(file).map_err(|e| {
14 ConduitError::Storage(format!(
15 "Failed to open archive '{}': {}",
16 path.as_ref().display(),
17 e
18 ))
19 })
20 }
21
22 pub fn create<P: AsRef<Path>>(path: P) -> ConduitResult<ZipWriter<File>> {
23 let file = File::create(&path)?;
24 Ok(ZipWriter::new(file))
25 }
26
27 pub fn read_metadata(archive: &mut ZipArchive<File>, name: &str) -> ConduitResult<String> {
28 let mut content = String::new();
29 let mut file = Self::get_validated_file(archive, name, 25 * 1024 * 1024)?;
30 file.read_to_string(&mut content).map_err(|e| {
31 ConduitError::Io(std::io::Error::other(format!(
32 "Failed to read metadata '{name}': {e}"
33 )))
34 })?;
35 Ok(content)
36 }
37
38 pub fn read_bytes(archive: &mut ZipArchive<File>, name: &str) -> ConduitResult<Vec<u8>> {
39 let mut buffer = Vec::new();
40 let mut file = Self::get_validated_file(archive, name, 100 * 1024 * 1024)?;
41 file.read_to_end(&mut buffer).map_err(|e| {
42 ConduitError::Io(std::io::Error::other(format!(
43 "Failed to read bytes from '{name}': {e}"
44 )))
45 })?;
46 Ok(buffer)
47 }
48
49 pub fn add_file<W: Write + std::io::Seek>(
50 writer: &mut ZipWriter<W>,
51 name: &str,
52 content: &[u8],
53 ) -> ConduitResult<()> {
54 let options: FileOptions<()> = FileOptions::default()
55 .compression_method(zip::CompressionMethod::Deflated)
56 .unix_permissions(0o644);
57
58 writer.start_file(name, options).map_err(|e| {
59 ConduitError::Storage(format!("Failed to create entry '{name}' in archive: {e}"))
60 })?;
61 writer.write_all(content)?;
62 Ok(())
63 }
64
65 fn get_validated_file<'a>(
66 archive: &'a mut ZipArchive<File>,
67 name: &str,
68 size_limit: u64,
69 ) -> ConduitResult<zip::read::ZipFile<'a, File>> {
70 let normalized_name = name.replace('\\', "/");
71
72 if normalized_name.contains("..") || normalized_name.starts_with('/') {
73 return Err(ConduitError::Storage(format!(
74 "Security violation: malicious path detected: {name}"
75 )));
76 }
77
78 let file = archive
79 .by_name(name)
80 .map_err(|_| ConduitError::NotFound(format!("Entry '{name}' not found in archive")))?;
81
82 if file.size() > size_limit {
83 return Err(ConduitError::Storage(format!(
84 "Security violation: entry '{name}' size ({} MB) exceeds limit ({} MB)",
85 file.size() / 1024 / 1024,
86 size_limit / 1024 / 1024
87 )));
88 }
89
90 Ok(file)
91 }
92
93 pub fn read_and_deserialize<T>(archive: &mut ZipArchive<File>, name: &str) -> ConduitResult<T>
94 where
95 T: serde::de::DeserializeOwned,
96 {
97 let raw = Self::read_metadata(archive, name)?;
98
99 let extension = Path::new(name)
100 .extension()
101 .and_then(|ext| ext.to_str())
102 .map(str::to_lowercase);
103
104 match extension.as_deref() {
105 Some("json") => serde_json::from_str(&raw)
106 .map_err(|e| ConduitError::Parsing(format!("JSON error in {name}: {e}"))),
107 Some("toml") => toml::from_str(&raw)
108 .map_err(|e| ConduitError::Parsing(format!("TOML error in {name}: {e}"))),
109 _ => Err(ConduitError::Parsing(format!(
110 "Unsupported or missing file extension for deserialization: {name}"
111 ))),
112 }
113 }
114
115 pub fn serialize_and_add<T, W>(
116 writer: &mut zip::ZipWriter<W>,
117 name: &str,
118 data: &T,
119 ) -> ConduitResult<()>
120 where
121 T: serde::Serialize,
122 W: std::io::Write + std::io::Seek,
123 {
124 let extension = std::path::Path::new(name)
125 .extension()
126 .and_then(|ext| ext.to_str())
127 .map(str::to_lowercase);
128
129 let bytes = match extension.as_deref() {
130 Some("json") => serde_json::to_vec(data)
131 .map_err(|e| ConduitError::Parsing(format!("JSON serialize error: {e}")))?,
132 Some("toml" | "lock") => toml::to_string(data)
133 .map_err(|e| ConduitError::Parsing(format!("TOML serialize error: {e}")))?
134 .into_bytes(),
135 _ => {
136 return Err(ConduitError::Parsing(
137 "Unsupported export format".to_string(),
138 ));
139 }
140 };
141
142 Self::add_file(writer, name, &bytes)
143 }
144
145 pub fn add_file_from_reader<W, R>(
146 writer: &mut ZipWriter<W>,
147 name: &str,
148 mut reader: R,
149 ) -> ConduitResult<()>
150 where
151 W: Write + std::io::Seek,
152 R: Read,
153 {
154 let options: FileOptions<()> = FileOptions::default()
155 .compression_method(zip::CompressionMethod::Deflated)
156 .unix_permissions(0o644);
157
158 writer.start_file(name, options).map_err(|e| {
159 ConduitError::Storage(format!("Failed to create entry '{name}' in archive: {e}"))
160 })?;
161
162 std::io::copy(&mut reader, writer).map_err(ConduitError::Io)?;
163
164 Ok(())
165 }
166}