1use std::collections::BTreeSet;
2use std::ffi::{OsStr, OsString};
3use std::fs;
4use std::io;
5use std::io::{Error, Write};
6use std::mem::size_of;
7use std::path::Path;
8
9use byteorder::{LittleEndian, WriteBytesExt};
10use log::debug;
11
12use crate::{
13 CORRUPTION_CHECK_BYTES, FILE_ID, FORMAT_VERSION, ItemKind, Location, Metadata, PATH_SEPARATOR,
14};
15
16pub struct Item {
17 kind: ItemKind,
18 contents: Vec<u8>,
19
20 path_os: OsString,
22}
23
24impl Item {
25 #[must_use]
26 pub fn file_name_bytes(&self) -> Vec<u8> {
27 self.path_os.to_string_lossy().as_bytes().to_owned()
28 }
29}
30
31pub struct BankWriter<WriterType: Write> {
32 inner: WriterType,
33 items: Vec<Item>,
34
35 written: bool,
37}
38
39impl<WriterType: Write> BankWriter<WriterType> {
40 pub fn new(inner: WriterType) -> BankWriter<WriterType> {
41 BankWriter {
42 inner,
43 items: Vec::new(),
44 written: false,
45 }
46 }
47
48 pub fn add(&mut self, kind: ItemKind, file_name: &OsStr, contents: Vec<u8>) -> io::Result<()> {
60 if self.written {
61 return Err(Error::other(
62 "Cannot add to a bank that has already been written",
63 ));
64 }
65
66 let file_name = if let Some(dir_name) = kind.directory() {
68 let mut path_str = OsString::from(dir_name);
69 path_str.push(PATH_SEPARATOR.to_string());
70 path_str.push(file_name);
71 path_str
72 } else {
73 file_name.to_owned()
74 };
75
76 self.items.push(Item {
77 kind,
78 contents,
79 path_os: file_name,
80 });
81 Ok(())
82 }
83
84 pub fn add_file<P: AsRef<Path>>(
92 &mut self,
93 kind: ItemKind,
94 file_name: &OsStr,
95 data_path: P,
96 ) -> io::Result<()> {
97 let contents = fs::read(data_path)?;
98 self.add(kind, file_name, contents)
99 }
100
101 pub fn add_metadata(&mut self, metadata: &Metadata) -> io::Result<()> {
107 let contents = if metadata.id.is_empty() {
109 let mut id_parts = Vec::with_capacity(2);
110 let author_part = Metadata::sanitize_id(&metadata.author);
111 let name_part = Metadata::sanitize_id(&metadata.name);
112 if !author_part.is_empty() {
113 id_parts.push(author_part);
114 }
115 if !name_part.is_empty() {
116 id_parts.push(name_part);
117 }
118 let metadata = Metadata {
119 version: metadata.version,
120 id: id_parts.join("."),
121 name: metadata.name.clone(),
122 author: metadata.author.clone(),
123 description: metadata.description.clone(),
124 hash: metadata.hash.clone(),
125 extra: metadata.extra.clone(),
126 };
127
128 serde_json::to_vec_pretty(&metadata)?
131 } else {
132 serde_json::to_vec_pretty(metadata)?
133 };
134
135 debug!(
136 "Adding metadata contents: {}",
137 String::from_utf8_lossy(&contents)
138 );
139 self.add(
140 ItemKind::Metadata,
141 OsStr::new(Metadata::FILE_NAME),
142 contents,
143 )
144 }
145
146 pub fn write(&mut self) -> io::Result<()> {
153 if self.written {
156 return Err(Error::other("The bank has already been written"));
157 }
158
159 if !self
161 .items
162 .iter()
163 .any(|item| item.kind == ItemKind::Metadata)
164 {
165 debug!("Adding default metadata");
166 self.add_metadata(&Metadata::default())?;
167 }
168
169 let kinds = self
170 .items
171 .iter()
172 .map(|item| item.kind)
173 .collect::<BTreeSet<ItemKind>>();
174 debug!("Kinds of items in this bank are {:?}", kinds);
175
176 self.inner.write_all(FILE_ID)?;
178 self.inner.write_all(CORRUPTION_CHECK_BYTES)?;
179 self.inner.write_all(FORMAT_VERSION)?;
180
181 let file_count = self.items.len();
183 let directory_count = kinds.iter().filter_map(ItemKind::directory).count();
184 let location_count = file_count + directory_count;
185 self.inner
186 .write_u64::<LittleEndian>(location_count as u64)?;
187 debug!("Number of location is {location_count}");
188
189 let location_block_start =
191 FILE_ID.len() + CORRUPTION_CHECK_BYTES.len() + FORMAT_VERSION.len() + size_of::<u64>();
192
193 let file_name_block_length: usize = kinds
194 .iter()
195 .map(|kind| {
196 let dir_name_len = kind.directory().map_or(0, |dir| dir.len() + 1);
198
199 let file_names_len = self
200 .items
201 .iter()
202 .map(|item| {
203 if item.kind == *kind {
204 item.file_name_bytes().len() + 1
205 } else {
206 0
207 }
208 })
209 .sum::<usize>();
210
211 file_names_len + dir_name_len
212 })
213 .sum();
214
215 let mut data_offset = (location_block_start
216 + (location_count * Location::BLOCK_SIZE)
217 + size_of::<u64>()
218 + file_name_block_length) as u64;
219
220 let mut file_name_block = Vec::new();
222 for kind in &kinds {
223 if let Some(directory) = kind.directory() {
225 debug!("Writing directory {directory}");
226 self.inner
227 .write_u64::<LittleEndian>(file_name_block.len() as u64)?;
228 file_name_block.extend_from_slice(directory.as_bytes());
229 file_name_block.push(0_u8);
230
231 self.inner.write_u64::<LittleEndian>(0)?; self.inner.write_u64::<LittleEndian>(0)?; }
234
235 for item in self.items.iter().filter(|item| item.kind == *kind) {
236 self.inner
237 .write_u64::<LittleEndian>(file_name_block.len() as u64)?;
238 file_name_block.extend(item.file_name_bytes());
239 file_name_block.push(0_u8);
240
241 let contents_len = item.contents.len() as u64;
242 self.inner.write_u64::<LittleEndian>(data_offset)?;
243 self.inner.write_u64::<LittleEndian>(contents_len)?;
244 data_offset += contents_len;
245 }
246 }
247
248 debug!("File name block length is {file_name_block_length}");
249 self.inner
250 .write_u64::<LittleEndian>(file_name_block_length as u64)?;
251 self.inner.write_all(&file_name_block)?;
252
253 for kind in kinds {
255 for item in self.items.iter().filter(|item| kind == item.kind) {
256 debug!(
257 "Writing item {} ({} bytes)",
258 item.path_os.to_string_lossy(),
259 item.contents.len()
260 );
261 self.inner.write_all(&item.contents)?;
262 }
263 }
264
265 self.inner.flush()?;
266 self.written = true;
267 Ok(())
268 }
269}