rs2cache/store/
disk_store.rs1use 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 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 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 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 #[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 #[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}