rscache/lib.rs
1//! A read-only, high-level, virtual file API for the RuneScape cache.
2//!
3//! This crate provides high performant data reads into the [Oldschool RuneScape] and [RuneScape 3]
4//! cache file systems. It can read the necessary data to synchronize the client's cache with the
5//! server. There are also some [loaders](#loaders) that give access to definitions from the cache
6//! such as items or npcs.
7//!
8//! For read-heavy workloads, a writer can be used to prevent continuous buffer allocations. By
9//! default every read will allocate a writer with the correct capacity.
10//!
11//! RuneScape's chat system uses huffman coding to compress messages. In order to decompress them
12//! this library has a [`Huffman`] implementation.
13//!
14//! When a RuneScape client sends game packets the id's are encoded and can be decoded with the
15//! [`IsaacRand`] implementation. These id's are encoded by the client in a predictable random order
16//! which can be reversed if the server has its own `IsaacRand` with the same encoder/decoder keys.
17//! These keys are sent by the client on login and are user specific. It will only send encoded
18//! packet id's if the packets are game packets.
19//!
20//! Note that this crate is still evolving; both OSRS & RS3 are not fully supported/implemented and
21//! will probably contain bugs or miss core features. If you require features or find bugs consider
22//! [opening an issue].
23//!
24//! # Safety
25//!
26//! In order to read bytes in a high performant way the cache uses [memmap2]. This can be unsafe
27//! because of its potential for _Undefined Behaviour_ when the underlying file is subsequently
28//! modified, in or out of process.
29//!
30//! Using `Mmap` here is safe because the RuneScape cache is a read-only binary file system. The map
31//! will remain valid even after the `File` is dropped, it's completely independent of the `File`
32//! used to create it. Therefore, the use of unsafe is not propagated outwards. When the `Cache` is
33//! dropped memory will be subsequently unmapped.
34//!
35//! # Features
36//!
37//! The cache's protocol defaults to OSRS. In order to use the RS3 protocol you can enable the `rs3`
38//! feature flag. A lot of types derive [serde]'s `Serialize` and `Deserialize`. The `serde-derive`
39//! feature flag can be used to enable (de)serialization on any compatible types.
40//!
41//! # Quick Start
42//!
43//! The recommended usage would be to wrap it using
44//! [`std::sync::LazyLock`](https://doc.rust-lang.org/std/sync/struct.LazyLock.html) making it the
45//! easiest way to access cache data from anywhere and at any time. No need for an `Arc` or a
46//! `Mutex` because `Cache` will always be `Send + Sync`.
47//! ```rust
48//! use rscache::Cache;
49//! use std::sync::LazyLock;
50//!
51//! static CACHE: LazyLock<Cache> = LazyLock::new(|| {
52//! Cache::new("./data/osrs_cache")
53//! .expect("cache files to be successfully memory mapped")
54//! });
55//!
56//! std::thread::spawn(|| -> Result<(), rscache::Error> {
57//! let buffer = CACHE.read(0, 10)?;
58//! Ok(())
59//! });
60//!
61//! std::thread::spawn(|| -> Result<(), rscache::Error> {
62//! let buffer = CACHE.read(0, 10)?;
63//! Ok(())
64//! });
65//! ```
66//!
67//! For an instance that stays local to this thread you can simply use:
68//! ```
69//! use rscache::Cache;
70//!
71//! # fn main() -> Result<(), rscache::Error> {
72//! let cache = Cache::new("./data/osrs_cache")
73//! .expect("cache files to be successfully memory mapped");
74//!
75//! let index_id = 2; // Config index.
76//! let archive_id = 10; // Archive containing item definitions.
77//!
78//! let buffer = cache.read(index_id, archive_id)?;
79//! # Ok(())
80//! # }
81//! ```
82//!
83//! If you want to share the instance over multiple threads you can do so by wrapping it in an
84//! [`Arc`](https://doc.rust-lang.org/std/sync/struct.Arc.html)
85//! ```
86//! use rscache::Cache;
87//! use std::sync::Arc;
88//!
89//! let cache = Arc::new(Cache::new("./data/osrs_cache")
90//! .expect("cache files to be successfully memory mapped"));
91//!
92//! let c = Arc::clone(&cache);
93//! std::thread::spawn(move || -> Result<(), rscache::Error> {
94//! // use the cloned handle
95//! let buffer = c.read(0, 10)?;
96//! Ok(())
97//! });
98//!
99//! std::thread::spawn(move || -> Result<(), rscache::Error> {
100//! // use handle directly and take ownership
101//! let buffer = cache.read(0, 10)?;
102//! Ok(())
103//! });
104//! ```
105//!
106//! # Loaders
107//!
108//! In order to get [definitions](crate::definition) you can look at the [loaders](crate::loader)
109//! this library provides. The loaders use the cache as a dependency to parse in their data and
110//! cache the relevant definitions internally. The loader module also tells you how to make a loader
111//! if this crate doesn't (yet) provide it.
112//!
113//! Note: Some loaders cache these definitions lazily because of either the size of the data or the
114//! performance. The map loader for example is both slow and large so caching is by default lazy.
115//! Lazy loaders require mutability.
116//!
117//! [Oldschool RuneScape]: https://oldschool.runescape.com/
118//! [RuneScape 3]: https://www.runescape.com/
119//! [opening an issue]: https://github.com/jimvdl/rs-cache/issues/new
120//! [serde]: https://crates.io/crates/serde
121//! [memmap2]: https://crates.io/crates/memmap2
122//! [`Huffman`]: crate::util::Huffman
123//! [`IsaacRand`]: crate::util::IsaacRand
124#![cfg_attr(docsrs, feature(doc_cfg))]
125#![deny(
126 clippy::all,
127 clippy::correctness,
128 clippy::suspicious,
129 clippy::style,
130 clippy::complexity,
131 clippy::perf
132)]
133
134#[macro_use]
135pub mod util;
136pub mod checksum;
137pub mod definition;
138pub mod error;
139pub mod extension;
140pub mod loader;
141
142#[doc(inline)]
143pub use error::Error;
144use error::Result;
145
146use checksum::Checksum;
147#[cfg(feature = "rs3")]
148use checksum::{RsaChecksum, RsaKeys};
149use runefs::codec::{Buffer, Decoded, Encoded};
150use runefs::error::{Error as RuneFsError, ReadError};
151use runefs::{ArchiveRef, Dat2, Indices, MAIN_DATA};
152use std::{io::Write, path::Path};
153
154/// A complete virtual representation of the RuneScape cache file system.
155#[derive(Debug)]
156pub struct Cache {
157 pub(crate) data: Dat2,
158 pub(crate) indices: Indices,
159}
160
161impl Cache {
162 /// Creates a high level virtual memory map over the cache directory.
163 ///
164 /// All files are isolated on allocation by keeping them as in-memory files.
165 ///
166 /// # Errors
167 ///
168 /// The bulk of the errors which might occur are mostely I/O related due to
169 /// acquiring file handles.
170 ///
171 /// Other errors might include protocol changes in newer caches. Any error
172 /// unrelated to I/O at this stage should be considered a bug.
173 pub fn new<P: AsRef<Path>>(path: P) -> crate::Result<Self> {
174 Ok(Self {
175 data: Dat2::new(path.as_ref().join(MAIN_DATA))?,
176 indices: Indices::new(path)?,
177 })
178 }
179
180 /// Generate a checksum based on the current cache.
181 ///
182 /// The `Checksum` acts as a validator for individual cache files. Any
183 /// RuneScape client will request a list of crc's to check the validity of
184 /// all of the file data that was transferred.
185 pub fn checksum(&self) -> crate::Result<Checksum> {
186 Checksum::new(self)
187 }
188
189 /// Generate a checksum based on the current cache with RSA encryption.
190 ///
191 /// `RsaChecksum` wraps a regular `Checksum` with the added benefit of
192 /// encrypting the whirlpool hash into the checksum buffer.
193 #[cfg(feature = "rs3")]
194 #[cfg_attr(docsrs, doc(cfg(feature = "rs3")))]
195 pub fn checksum_with<'a>(&self, keys: RsaKeys<'a>) -> crate::Result<RsaChecksum<'a>> {
196 RsaChecksum::with_keys(self, keys)
197 }
198
199 /// Retrieves and constructs data corresponding to the given index and
200 /// archive.
201 ///
202 /// # Errors
203 ///
204 /// When trying to retrieve data from an index or an archive that does not
205 /// exist the `IndexNotFound` or `ArchiveNotFound` errors are returned,
206 /// respectively.
207 ///
208 /// Any other errors such as sector validation failures or failed parsers
209 /// should be considered a bug.
210 pub fn read(&self, index_id: u8, archive_id: u32) -> crate::Result<Buffer<Encoded>> {
211 let index = self
212 .indices
213 .get(&index_id)
214 .ok_or(RuneFsError::Read(ReadError::IndexNotFound(index_id)))?;
215
216 let archive = index
217 .archive_refs
218 .get(&archive_id)
219 .ok_or(RuneFsError::Read(ReadError::ArchiveNotFound {
220 idx: index_id,
221 arc: archive_id,
222 }))?;
223
224 let buffer = self.data.read(archive)?;
225
226 assert_eq!(buffer.len(), archive.length);
227
228 Ok(buffer)
229 }
230
231 pub(crate) fn read_archive(&self, archive: &ArchiveRef) -> crate::Result<Buffer<Encoded>> {
232 self.read(archive.index_id, archive.id)
233 }
234
235 /// Retrieves and writes data corresponding to the given index and archive
236 /// into `W`.
237 ///
238 /// # Errors
239 ///
240 /// See the error section on [`read`](Cache::read) for more details.
241 pub fn read_into_writer<W: Write>(
242 &self,
243 index_id: u8,
244 archive_id: u32,
245 writer: &mut W,
246 ) -> crate::Result<()> {
247 let index = self
248 .indices
249 .get(&index_id)
250 .ok_or(RuneFsError::Read(ReadError::IndexNotFound(index_id)))?;
251
252 let archive = index
253 .archive_refs
254 .get(&archive_id)
255 .ok_or(RuneFsError::Read(ReadError::ArchiveNotFound {
256 idx: index_id,
257 arc: archive_id,
258 }))?;
259 Ok(self.data.read_into_writer(archive, writer)?)
260 }
261
262 /// Retrieves the huffman table.
263 ///
264 /// Required when decompressing chat messages, see
265 /// [`Huffman`](crate::util::Huffman).
266 pub fn huffman_table(&self) -> crate::Result<Buffer<Decoded>> {
267 let index_id = 10;
268
269 let archive = self.archive_by_name(index_id, "huffman")?;
270 let buffer = self.read_archive(archive)?;
271
272 assert_eq!(buffer.len(), archive.length);
273
274 Ok(buffer.decode()?)
275 }
276
277 pub(crate) fn archive_by_name<T: AsRef<str>>(
278 &self,
279 index_id: u8,
280 name: T,
281 ) -> crate::Result<&ArchiveRef> {
282 let index = self
283 .indices
284 .get(&index_id)
285 .ok_or(RuneFsError::Read(ReadError::IndexNotFound(index_id)))?;
286 let hash = util::djd2::hash(&name);
287
288 let archive = index
289 .metadata
290 .iter()
291 .find(|archive| archive.name_hash == hash)
292 .ok_or_else(|| crate::error::NameHashMismatch {
293 hash,
294 name: name.as_ref().into(),
295 idx: index_id,
296 })?;
297
298 let archive_ref = index
299 .archive_refs
300 .get(&archive.id)
301 .ok_or(RuneFsError::Read(ReadError::ArchiveNotFound {
302 idx: index_id,
303 arc: archive.id,
304 }))?;
305
306 Ok(archive_ref)
307 }
308}
309
310#[cfg(test)]
311fn is_normal<T: Send + Sync + Sized + Unpin>() {}
312#[test]
313fn normal_types() {
314 is_normal::<Cache>();
315}