rs2cache/store/
disk_store.rs

1use super::{Store, StoreError, DATA_PATH, LEGACY_DATA_PATH};
2use memmap2::Mmap;
3use osrs_bytes::ReadExt;
4use std::{cmp, collections::HashMap, fs::File, io::Cursor, path::Path};
5use thiserror::Error;
6
7const EXTENDED_BLOCK_HEADER_SIZE: usize = 10;
8const BLOCK_HEADER_SIZE: usize = 8;
9const EXTENDED_BLOCK_DATA_SIZE: usize = 510;
10const BLOCK_DATA_SIZE: usize = 512;
11const MUSIC_ARCHIVE: u8 = 40;
12const BLOCK_SIZE: usize = BLOCK_HEADER_SIZE + BLOCK_DATA_SIZE;
13const INDEX_ENTRY_SIZE: usize = 6;
14
15const INDEX_PATH: &str = "main_file_cache.idx";
16const MUSIC_DATA_PATH: &str = "main_file_cache.dat2m";
17
18const MAX_ARCHIVE: usize = 255;
19
20#[derive(Error, Debug)]
21pub enum DiskStoreError {
22    #[error("io error: {0}")]
23    Io(#[from] std::io::Error),
24    #[error("failed converting root to string")]
25    RootToString,
26    #[error("failed getting music data")]
27    MusicData,
28    #[error("file not found")]
29    FileNotFound,
30}
31
32struct IndexEntry {
33    size: u32,
34    block: u32,
35}
36
37pub struct DiskStore {
38    _root: String,
39    data: Mmap,
40    music_data: Option<Mmap>,
41    indexes: HashMap<usize, Mmap>,
42    legacy: bool,
43}
44
45impl DiskStore {
46    pub fn open<P: AsRef<Path>>(path: P) -> Result<DiskStore, DiskStoreError> {
47        let js5_data_path = Path::new(path.as_ref()).join(DATA_PATH);
48        let legacy_data_path = Path::new(path.as_ref()).join(LEGACY_DATA_PATH);
49
50        // We check for js5_data_path first as it takes precedence.
51        let legacy = !js5_data_path.exists();
52
53        let data_path = if legacy {
54            legacy_data_path
55        } else {
56            js5_data_path
57        };
58
59        let data = unsafe { Mmap::map(&File::open(data_path)?) }?;
60
61        let music_data_path = Path::new(path.as_ref()).join(MUSIC_DATA_PATH);
62        let music_data = if music_data_path.exists() {
63            Some(unsafe { Mmap::map(&File::open(music_data_path)?)? })
64        } else {
65            None
66        };
67
68        let mut archives = HashMap::new();
69        for i in 0..MAX_ARCHIVE + 1 {
70            let path = Path::new(path.as_ref()).join(format!("{INDEX_PATH}{i}"));
71            if Path::new(&path).exists() {
72                let index = unsafe { Mmap::map(&File::open(&path)?)? };
73                archives.insert(i, index);
74            }
75        }
76
77        Ok(DiskStore {
78            _root: String::from(path.as_ref().to_str().ok_or(DiskStoreError::RootToString)?),
79            data,
80            music_data,
81            indexes: archives,
82            legacy,
83        })
84    }
85
86    fn get_data(&self, archive: u8) -> Result<&Mmap, DiskStoreError> {
87        if archive == MUSIC_ARCHIVE && self.music_data.is_some() {
88            Ok(self.music_data.as_ref().ok_or(DiskStoreError::MusicData)?)
89        } else {
90            Ok(&self.data)
91        }
92    }
93
94    fn read_index_entry(&self, archive: u8, group: u32) -> Result<IndexEntry, DiskStoreError> {
95        let index = &self.indexes[&(archive as usize)];
96
97        let pos = (group as usize) * INDEX_ENTRY_SIZE;
98        if pos + INDEX_ENTRY_SIZE > index.len() {
99            return Err(DiskStoreError::FileNotFound);
100        }
101
102        let mut csr = Cursor::new(index);
103        csr.set_position(pos as u64);
104
105        let size = csr.read_u24()?;
106        let block = csr.read_u24()?;
107
108        Ok(IndexEntry { size, block })
109    }
110}
111
112impl Store for DiskStore {
113    fn list(&self, archive: u8) -> Result<Vec<u32>, StoreError> {
114        let index = &self.indexes[&(archive as usize)];
115        let mut index_csr = Cursor::new(index);
116
117        let mut groups = Vec::new();
118        let mut group = 0;
119        while index_csr.read_u24().is_ok() {
120            let block = index_csr.read_u24()?;
121            if block != 0 {
122                groups.push(group);
123            }
124
125            group += 1;
126        }
127
128        Ok(groups)
129    }
130
131    fn read(&self, archive: u8, group: u32) -> Result<Vec<u8>, StoreError> {
132        let entry = self.read_index_entry(archive, group)?;
133        if entry.block == 0 {
134            return Err(StoreError::GroupTooShort);
135        }
136
137        let mut buf = Vec::with_capacity(entry.size as usize);
138        let data = self.get_data(archive)?;
139
140        let extended = group >= 65536;
141        let header_size = if extended {
142            EXTENDED_BLOCK_HEADER_SIZE
143        } else {
144            BLOCK_HEADER_SIZE
145        };
146        let data_size = if extended {
147            EXTENDED_BLOCK_DATA_SIZE
148        } else {
149            BLOCK_DATA_SIZE
150        };
151
152        let mut block = entry.block;
153        let mut num = 0;
154
155        while buf.len() < entry.size as usize {
156            if block == 0 {
157                return Err(StoreError::GroupTooShort);
158            }
159
160            let pos = (block * BLOCK_SIZE as u32) as usize;
161            if pos + header_size > self.data.len() {
162                return Err(StoreError::NextBlockOutsideDataFile);
163            }
164
165            let mut data_csr = Cursor::new(&data);
166            data_csr.set_position(pos as u64);
167
168            let actual_group = if extended {
169                data_csr.read_u32()?
170            } else {
171                data_csr.read_u16()? as u32
172            };
173            let actual_num = data_csr.read_u16()?;
174            let next_block = data_csr.read_u24()?;
175            let actual_archive = data_csr.read_u8()? - (if self.legacy { 1 } else { 0 });
176
177            if actual_group != group {
178                return Err(StoreError::GroupMismatch(group, actual_group));
179            }
180            if actual_num != num {
181                return Err(StoreError::BlockMismatch(num, actual_num));
182            }
183            if actual_archive != archive {
184                return Err(StoreError::ArchiveMismatch(archive, actual_archive));
185            }
186
187            // read data
188            let len = cmp::min(entry.size as usize - buf.len(), data_size);
189            buf.extend_from_slice(&data[pos + header_size..pos + header_size + len]);
190
191            // advance to next block
192            block = next_block;
193            num += 1;
194        }
195
196        Ok(buf)
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_list_groups() {
206        read_test("single-block", |store| {
207            assert_eq!(vec![1], store.list(255).unwrap());
208        });
209        read_test("fragmented", |store| {
210            assert_eq!(vec![0, 1], store.list(255).unwrap());
211        });
212        read_test("single-block-extended", |store| {
213            assert_eq!(vec![65536], store.list(255).unwrap());
214        });
215    }
216
217    // TODO: Handle this error
218    /*#[test]
219    fn test_list_non_existent() {
220        read_test("empty", |store| {
221            assert_eq!(vec![1], store.list(255));
222        });
223    }*/
224
225    #[test]
226    fn test_read_single_block() {
227        read_test("single-block", |store| {
228            let actual = store.read(255, 1).unwrap();
229            let expected = "OpenRS2".as_bytes();
230            assert_eq!(expected, actual);
231        });
232    }
233
234    #[test]
235    fn test_read_single_block_extended() {
236        read_test("single-block-extended", |store| {
237            let actual = store.read(255, 65536).unwrap();
238            let expected = "OpenRS2".as_bytes();
239            assert_eq!(expected, actual);
240        });
241    }
242
243    #[test]
244    fn test_read_two_blocks() {
245        read_test("two-blocks", |store| {
246            let actual = store.read(255, 1).unwrap();
247            let expected = "OpenRS2".repeat(100).into_bytes();
248            assert_eq!(expected, actual);
249        });
250    }
251
252    #[test]
253    fn test_read_two_blocks_extended() {
254        read_test("two-blocks-extended", |store| {
255            let actual = store.read(255, 65536).unwrap();
256            let expected = "OpenRS2".repeat(100).into_bytes();
257            assert_eq!(expected, actual);
258        });
259    }
260
261    #[test]
262    fn test_read_multiple_blocks() {
263        read_test("multiple-blocks", |store| {
264            let actual = store.read(255, 1).unwrap();
265            let expected = "OpenRS2".repeat(1000).into_bytes();
266            assert_eq!(expected, actual);
267        });
268    }
269
270    #[test]
271    fn test_read_multiple_blocks_extended() {
272        read_test("multiple-blocks-extended", |store| {
273            let actual = store.read(255, 65536).unwrap();
274            let expected = "OpenRS2".repeat(1000).into_bytes();
275            assert_eq!(expected, actual);
276        });
277    }
278
279    // TODO: Error handling here, simply follow the trace of error and handle accordingly
280    /*#[test]
281    fn test_read_non_existent() {
282        read_test("single-block", |store| {
283            store.read(0, 0);
284            store.read(255, 0);
285            store.read(255, 2);
286        });
287    }*/
288
289    #[test]
290    fn test_read_fragmented() {
291        read_test("fragmented", |store| {
292            let actual = store.read(255, 1).unwrap();
293            let expected = "OpenRS2".repeat(100).into_bytes();
294            assert_eq!(expected, actual);
295        });
296    }
297
298    fn read_test<P, F>(p: P, f: F)
299    where
300        P: AsRef<Path>,
301        F: FnOnce(DiskStore),
302    {
303        f(DiskStore::open(Path::new("tests/data/disk-store").join(p)).unwrap())
304    }
305}