Skip to main content

kibank/
write.rs

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 of the file within the bank, including any leading directory.
21    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    /// If the data has already been committed with a call to `write()`.
36    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    /// Adding an item with empty contents results in the item being treated as a directory
49    /// instead of a file. It is a limitation of the format that there is no way to have
50    /// zero-length contents.
51    ///
52    /// * `kind` - type of the file
53    /// * `file_name` - name of the file within the bank, without any leading directory
54    /// * `contents` - the data to include in the bank
55    ///
56    /// # Errors
57    ///
58    /// Will return `Err` if the bank has already been written
59    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        // Add the leading directory so the item is ready to use.
67        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    /// * `kind` - type of the file
85    /// * `file_name` - name of the file within the bank
86    /// * `data_path` - location of the file that contains the data to include
87    ///
88    /// # Errors
89    ///
90    /// Will return `Err` if the bank has already been written
91    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    /// A default ID will be created if one is not provided.
102    ///
103    /// # Errors
104    ///
105    /// Will return `Err` if the bank has already been written
106    pub fn add_metadata(&mut self, metadata: &Metadata) -> io::Result<()> {
107        // Create the ID from the author and name if there is none.
108        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            // Pretty-print the JSON to match what Bank Maker does. Bank
129            // Maker uses \n\r end of line on Windows and \n on Mac.
130            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    /// Commit the contents added to the bank. All bytes will be written to the
147    /// underlying stream before returning.
148    ///
149    /// # Errors
150    ///
151    /// Will return `Err` if the bank has already been written
152    pub fn write(&mut self) -> io::Result<()> {
153        // The file is written in one pass, without seeking backwards, to allow
154        // the possibility of streaming the output.
155        if self.written {
156            return Err(Error::other("The bank has already been written"));
157        }
158
159        // Include metadata if it hasn't been provided.
160        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        // Header
177        self.inner.write_all(FILE_ID)?;
178        self.inner.write_all(CORRUPTION_CHECK_BYTES)?;
179        self.inner.write_all(FORMAT_VERSION)?;
180
181        // Number of files and directories added to the bank.
182        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        // Offsets
190        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                // All the filenames and directory names for the kind.
197                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        // Locations
221        let mut file_name_block = Vec::new();
222        for kind in &kinds {
223            // Some kinds of items require a directory entry.
224            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)?; // Data offset
232                self.inner.write_u64::<LittleEndian>(0)?; // Data size
233            }
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        // Write the contents of each item.
254        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}