1use std::io::{self, Write, Seek};
2use zip::{ZipWriter, write::SimpleFileOptions, CompressionMethod};
3
4pub struct ZipDocumentWriter<W: Write + Seek> {
6 pub(crate) zip: ZipWriter<W>,
7 pub(crate) manifest: serde_json::Value,
8}
9
10impl<W: Write + Seek> ZipDocumentWriter<W> {
11 pub fn new(w: W) -> io::Result<Self> {
13 let zip = ZipWriter::new(w);
14 let manifest = serde_json::json!({ "version": 1, "entries": [] });
15 Ok(Self { zip, manifest })
16 }
17 pub fn manifest(&self) -> &serde_json::Value {
19 &self.manifest
20 }
21 pub fn manifest_mut(&mut self) -> &mut serde_json::Value {
23 &mut self.manifest
24 }
25 pub fn set_manifest(
27 &mut self,
28 manifest: serde_json::Value,
29 ) -> &mut Self {
30 self.manifest = manifest;
31 self
32 }
33 #[cfg_attr(feature = "dev-tracing", tracing::instrument(skip(self, value), fields(
35 crate_name = "file",
36 file_name = %name
37 )))]
38 pub fn add_json(
39 &mut self,
40 name: &str,
41 value: &serde_json::Value,
42 ) -> io::Result<()> {
43 let opts = SimpleFileOptions::default()
44 .compression_method(CompressionMethod::Deflated);
45 self.zip.start_file(name, opts)?;
46 let data = serde_json::to_vec(value).map_err(io::Error::other)?;
47 if let Some(entries) =
49 self.manifest.get_mut("entries").and_then(|v| v.as_array_mut())
50 {
51 entries.push(serde_json::json!({
52 "name": name,
53 "kind": "json",
54 "logical_len": data.len(),
55 "compression": "deflate"
56 }));
57 }
58 self.zip.write_all(&data)
59 }
60 pub fn add_stored(
62 &mut self,
63 name: &str,
64 bytes: &[u8],
65 ) -> io::Result<()> {
66 let opts = SimpleFileOptions::default()
67 .compression_method(CompressionMethod::Stored);
68 self.zip.start_file(name, opts)?;
69 if let Some(entries) =
70 self.manifest.get_mut("entries").and_then(|v| v.as_array_mut())
71 {
72 entries.push(serde_json::json!({
73 "name": name,
74 "kind": "binary",
75 "logical_len": bytes.len(),
76 "compression": "stored"
77 }));
78 }
79 self.zip.write_all(bytes)
80 }
81
82 pub fn add_plugin_state(
84 &mut self,
85 plugin_name: &str,
86 state_data: &[u8],
87 ) -> io::Result<()> {
88 let plugin_file_path = format!("plugins/{plugin_name}");
89 let opts = SimpleFileOptions::default()
90 .compression_method(CompressionMethod::Deflated);
91 self.zip.start_file(&plugin_file_path, opts)?;
92
93 if let Some(entries) =
94 self.manifest.get_mut("entries").and_then(|v| v.as_array_mut())
95 {
96 entries.push(serde_json::json!({
97 "name": plugin_file_path,
98 "kind": "plugin_state",
99 "plugin": plugin_name,
100 "logical_len": state_data.len(),
101 "compression": "deflate"
102 }));
103 }
104 self.zip.write_all(state_data)
105 }
106
107 pub fn add_plugin_states<I>(
109 &mut self,
110 plugin_states: I,
111 ) -> io::Result<()>
112 where
113 I: IntoIterator<Item = (String, Vec<u8>)>,
114 {
115 for (plugin_name, state_data) in plugin_states {
116 self.add_plugin_state(&plugin_name, &state_data)?;
117 }
118 Ok(())
119 }
120 pub fn add_deflated(
122 &mut self,
123 name: &str,
124 bytes: &[u8],
125 ) -> io::Result<()> {
126 let opts = SimpleFileOptions::default()
127 .compression_method(CompressionMethod::Deflated);
128 self.zip.start_file(name, opts)?;
129 if let Some(entries) =
130 self.manifest.get_mut("entries").and_then(|v| v.as_array_mut())
131 {
132 entries.push(serde_json::json!({
133 "name": name,
134 "kind": "binary",
135 "logical_len": bytes.len(),
136 "compression": "deflate"
137 }));
138 }
139 self.zip.write_all(bytes)
140 }
141 pub fn finalize(mut self) -> io::Result<W> {
143 let opts = SimpleFileOptions::default()
144 .compression_method(CompressionMethod::Deflated);
145 self.zip.start_file("manifest.json", opts)?;
146 let data =
147 serde_json::to_vec(&self.manifest).map_err(io::Error::other)?;
148 self.zip.write_all(&data)?;
149 self.zip.finish().map_err(io::Error::other)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use std::io::Cursor;
157
158 #[test]
159 fn test_plugin_state_export() {
160 let buffer = Vec::new();
161 let cursor = Cursor::new(buffer);
162 let mut writer = ZipDocumentWriter::new(cursor).unwrap();
163
164 writer.add_plugin_state("test_plugin", b"test state data").unwrap();
166 writer
167 .add_plugin_state("another_plugin", b"another state data")
168 .unwrap();
169
170 let result = writer.finalize().unwrap();
171 let final_data = result.into_inner();
172
173 assert!(!final_data.is_empty());
175
176 let cursor = Cursor::new(&final_data);
178 let mut reader = crate::zipdoc::ZipDocumentReader::new(cursor).unwrap();
179
180 let plugins = reader.list_plugins().unwrap();
181 assert_eq!(plugins.len(), 2);
182 assert!(plugins.contains(&"test_plugin".to_string()));
183 assert!(plugins.contains(&"another_plugin".to_string()));
184
185 let test_state =
187 reader.read_plugin_state("test_plugin").unwrap().unwrap();
188 assert_eq!(test_state, b"test state data");
189 }
190
191 #[test]
192 fn test_batch_plugin_states() {
193 let buffer = Vec::new();
194 let cursor = Cursor::new(buffer);
195 let mut writer = ZipDocumentWriter::new(cursor).unwrap();
196
197 let plugin_states = vec![
198 ("plugin1".to_string(), b"state1".to_vec()),
199 ("plugin2".to_string(), b"state2".to_vec()),
200 ("plugin3".to_string(), b"state3".to_vec()),
201 ];
202
203 writer.add_plugin_states(plugin_states.clone()).unwrap();
204 let result = writer.finalize().unwrap();
205 let final_data = result.into_inner();
206
207 let cursor = Cursor::new(&final_data);
208 let mut reader = crate::zipdoc::ZipDocumentReader::new(cursor).unwrap();
209
210 let all_states = reader.read_all_plugin_states().unwrap();
211 assert_eq!(all_states.len(), 3);
212
213 for (name, expected_data) in plugin_states {
214 let actual_data = all_states.get(&name).unwrap();
215 assert_eq!(actual_data, &expected_data);
216 }
217 }
218}