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}