Skip to main content

syntect/
dumps.rs

1//! Methods for dumping serializable structs to a compressed binary format,
2//! used to allow fast startup times
3//!
4//! Currently syntect serializes [`SyntaxSet`] structs with [`dump_to_uncompressed_file`]
5//! into `.packdump` files and likewise [`ThemeSet`] structs to `.themedump` files with [`dump_to_file`].
6//!
7//! You can use these methods to manage your own caching of compiled syntaxes and
8//! themes. And even your own `serde::Serialize` structures if you want to
9//! be consistent with your format.
10//!
11//! [`SyntaxSet`]: ../parsing/struct.SyntaxSet.html
12//! [`dump_to_uncompressed_file`]: fn.dump_to_uncompressed_file.html
13//! [`ThemeSet`]: ../highlighting/struct.ThemeSet.html
14//! [`dump_to_file`]: fn.dump_to_file.html
15#[cfg(feature = "default-themes")]
16use crate::highlighting::ThemeSet;
17#[cfg(feature = "default-syntaxes")]
18use crate::parsing::SyntaxSet;
19#[cfg(feature = "dump-load")]
20use bincode::deserialize_from;
21#[cfg(feature = "dump-create")]
22use bincode::serialize_into;
23#[cfg(feature = "dump-load")]
24use flate2::bufread::ZlibDecoder;
25#[cfg(feature = "dump-create")]
26use flate2::write::ZlibEncoder;
27#[cfg(feature = "dump-create")]
28use flate2::Compression;
29#[cfg(feature = "dump-load")]
30use serde::de::DeserializeOwned;
31#[cfg(feature = "dump-create")]
32use serde::ser::Serialize;
33use std::fs::File;
34#[cfg(feature = "dump-load")]
35use std::io::BufRead;
36#[cfg(feature = "dump-create")]
37use std::io::{BufWriter, Write};
38use std::path::Path;
39
40/// An error that can occur during dump/load operations
41#[derive(Debug, thiserror::Error)]
42#[non_exhaustive]
43pub enum DumpError {
44    /// An IO error occurred
45    #[error("IO error: {0}")]
46    Io(#[from] std::io::Error),
47    /// A serialization or deserialization error occurred
48    #[error("Serialization error: {0}")]
49    Serialize(#[source] Box<dyn std::error::Error + Send + Sync>),
50}
51
52/// Dumps an object to the given writer in a compressed binary format
53///
54/// The writer is encoded with the `bincode` crate and compressed with `flate2`.
55#[cfg(feature = "dump-create")]
56pub fn dump_to_writer<T: Serialize, W: Write>(to_dump: &T, output: W) -> Result<(), DumpError> {
57    serialize_to_writer_impl(to_dump, output, true)
58}
59
60/// Dumps an object to a binary array in the same format as [`dump_to_writer`]
61///
62/// [`dump_to_writer`]: fn.dump_to_writer.html
63#[cfg(feature = "dump-create")]
64pub fn dump_binary<T: Serialize>(o: &T) -> Vec<u8> {
65    let mut v = Vec::new();
66    dump_to_writer(o, &mut v).unwrap();
67    v
68}
69
70/// Dumps an encodable object to a file at a given path, in the same format as [`dump_to_writer`]
71///
72/// If a file already exists at that path it will be overwritten. The files created are encoded with
73/// the `bincode` crate and then compressed with the `flate2` crate.
74///
75/// [`dump_to_writer`]: fn.dump_to_writer.html
76#[cfg(feature = "dump-create")]
77pub fn dump_to_file<T: Serialize, P: AsRef<Path>>(o: &T, path: P) -> Result<(), DumpError> {
78    let out = BufWriter::new(File::create(path)?);
79    dump_to_writer(o, out)
80}
81
82/// A helper function for decoding and decompressing data from a reader
83#[cfg(feature = "dump-load")]
84pub fn from_reader<T: DeserializeOwned, R: BufRead>(input: R) -> Result<T, DumpError> {
85    deserialize_from_reader_impl(input, true)
86}
87
88/// Returns a fully loaded object from a binary dump.
89///
90/// This function panics if the dump is invalid.
91#[cfg(feature = "dump-load")]
92pub fn from_binary<T: DeserializeOwned>(v: &[u8]) -> T {
93    from_reader(v).unwrap()
94}
95
96/// Returns a fully loaded object from a binary dump file.
97#[cfg(feature = "dump-load")]
98pub fn from_dump_file<T: DeserializeOwned, P: AsRef<Path>>(path: P) -> Result<T, DumpError> {
99    let contents = std::fs::read(path)?;
100    from_reader(&contents[..])
101}
102
103/// To be used when serializing a [`SyntaxSet`] to a file. A [`SyntaxSet`]
104/// itself shall not be compressed, because the data for its lazy-loaded
105/// syntaxes are already compressed. Compressing another time just results in
106/// bad performance.
107#[cfg(feature = "dump-create")]
108pub fn dump_to_uncompressed_file<T: Serialize, P: AsRef<Path>>(
109    o: &T,
110    path: P,
111) -> Result<(), DumpError> {
112    let out = BufWriter::new(File::create(path)?);
113    serialize_to_writer_impl(o, out, false)
114}
115
116/// To be used when deserializing a [`SyntaxSet`] that was previously written to
117/// file using [dump_to_uncompressed_file].
118#[cfg(feature = "dump-load")]
119pub fn from_uncompressed_dump_file<T: DeserializeOwned, P: AsRef<Path>>(
120    path: P,
121) -> Result<T, DumpError> {
122    let contents = std::fs::read(path)?;
123    deserialize_from_reader_impl(&contents[..], false)
124}
125
126/// To be used when deserializing a [`SyntaxSet`] from raw data, for example
127/// data that has been embedded in your own binary with the [`include_bytes!`]
128/// macro.
129#[cfg(feature = "dump-load")]
130pub fn from_uncompressed_data<T: DeserializeOwned>(v: &[u8]) -> Result<T, DumpError> {
131    deserialize_from_reader_impl(v, false)
132}
133
134/// Private low level helper function used to implement the public API.
135#[cfg(feature = "dump-create")]
136fn serialize_to_writer_impl<T: Serialize, W: Write>(
137    to_dump: &T,
138    output: W,
139    use_compression: bool,
140) -> Result<(), DumpError> {
141    if use_compression {
142        let mut encoder = std::io::BufWriter::new(ZlibEncoder::new(output, Compression::best()));
143        serialize_into(&mut encoder, to_dump).map_err(|e| DumpError::Serialize(Box::new(e)))
144    } else {
145        serialize_into(output, to_dump).map_err(|e| DumpError::Serialize(Box::new(e)))
146    }
147}
148
149/// Private low level helper function used to implement the public API.
150#[cfg(feature = "dump-load")]
151fn deserialize_from_reader_impl<T: DeserializeOwned, R: BufRead>(
152    input: R,
153    use_compression: bool,
154) -> Result<T, DumpError> {
155    if use_compression {
156        let mut decoder = ZlibDecoder::new(input);
157        deserialize_from(&mut decoder).map_err(|e| DumpError::Serialize(Box::new(e)))
158    } else {
159        deserialize_from(input).map_err(|e| DumpError::Serialize(Box::new(e)))
160    }
161}
162
163#[cfg(feature = "default-syntaxes")]
164impl SyntaxSet {
165    /// Instantiates a new syntax set from a binary dump of Sublime Text's default open source
166    /// syntax definitions.
167    ///
168    /// These dumps are included in this library's binary for convenience.
169    ///
170    /// This method loads the version for parsing line strings with no `\n` characters at the end.
171    /// If you're able to efficiently include newlines at the end of strings, use
172    /// [`load_defaults_newlines`] since it works better. See [`SyntaxSetBuilder::add_from_folder`]
173    /// for more info on this issue.
174    ///
175    /// This is the recommended way of creating a syntax set for non-advanced use cases. It is also
176    /// significantly faster than loading the YAML files.
177    ///
178    /// Note that you can load additional syntaxes after doing this. If you want you can even use
179    /// the fact that SyntaxDefinitions are serializable with the bincode crate to cache dumps of
180    /// additional syntaxes yourself.
181    ///
182    /// [`load_defaults_newlines`]: #method.load_defaults_nonewlines
183    /// [`SyntaxSetBuilder::add_from_folder`]: struct.SyntaxSetBuilder.html#method.add_from_folder
184    pub fn load_defaults_nonewlines() -> SyntaxSet {
185        #[cfg(feature = "metadata")]
186        {
187            let mut ps: SyntaxSet =
188                from_uncompressed_data(include_bytes!("../assets/default_nonewlines.packdump"))
189                    .unwrap();
190            let metadata = from_binary(include_bytes!("../assets/default_metadata.packdump"));
191            ps.metadata = metadata;
192            ps
193        }
194        #[cfg(not(feature = "metadata"))]
195        {
196            from_uncompressed_data(include_bytes!("../assets/default_nonewlines.packdump")).unwrap()
197        }
198    }
199
200    /// Same as [`load_defaults_nonewlines`] but for parsing line strings with newlines at the end.
201    ///
202    /// These are separate methods because thanks to linker garbage collection, only the serialized
203    /// dumps for the method(s) you call will be included in the binary (each is ~200kb for now).
204    ///
205    /// [`load_defaults_nonewlines`]: #method.load_defaults_nonewlines
206    pub fn load_defaults_newlines() -> SyntaxSet {
207        #[cfg(feature = "metadata")]
208        {
209            let mut ps: SyntaxSet =
210                from_uncompressed_data(include_bytes!("../assets/default_newlines.packdump"))
211                    .unwrap();
212            let metadata = from_binary(include_bytes!("../assets/default_metadata.packdump"));
213            ps.metadata = metadata;
214            ps
215        }
216        #[cfg(not(feature = "metadata"))]
217        {
218            from_uncompressed_data(include_bytes!("../assets/default_newlines.packdump")).unwrap()
219        }
220    }
221}
222
223#[cfg(feature = "default-themes")]
224impl ThemeSet {
225    /// Loads the set of default themes
226    /// Currently includes (these are the keys for the map):
227    ///
228    /// - `base16-ocean.dark`,`base16-eighties.dark`,`base16-mocha.dark`,`base16-ocean.light`
229    /// - `InspiredGitHub` from [here](https://github.com/sethlopezme/InspiredGitHub.tmtheme)
230    /// - `Solarized (dark)` and `Solarized (light)`
231    pub fn load_defaults() -> ThemeSet {
232        from_binary(include_bytes!("../assets/default.themedump"))
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    #[cfg(all(
239        feature = "yaml-load",
240        feature = "dump-create",
241        feature = "dump-load",
242        feature = "parsing"
243    ))]
244    #[test]
245    fn can_dump_and_load() {
246        use super::*;
247        use crate::utils::testdata;
248
249        let ss = &*testdata::PACKAGES_SYN_SET;
250
251        let bin = dump_binary(&ss);
252        println!("{:?}", bin.len());
253        let ss2: SyntaxSet = from_binary(&bin[..]);
254        assert_eq!(ss.syntaxes().len(), ss2.syntaxes().len());
255    }
256
257    #[cfg(all(feature = "yaml-load", feature = "dump-create", feature = "dump-load"))]
258    #[test]
259    fn dump_is_deterministic() {
260        use super::*;
261        use crate::parsing::SyntaxSetBuilder;
262        use crate::utils::testdata;
263
264        let ss1 = &*testdata::PACKAGES_SYN_SET;
265        let bin1 = dump_binary(&ss1);
266
267        let mut builder2 = SyntaxSetBuilder::new();
268        builder2
269            .add_from_folder("testdata/Packages", false)
270            .unwrap();
271        let ss2 = builder2.build();
272        let bin2 = dump_binary(&ss2);
273        // This is redundant, but assert_eq! can be really slow on a large
274        // vector, so check the length first to fail faster.
275        assert_eq!(bin1.len(), bin2.len());
276        assert_eq!(bin1, bin2);
277    }
278
279    #[cfg(feature = "default-themes")]
280    #[test]
281    fn has_default_themes() {
282        use crate::highlighting::ThemeSet;
283        let themes = ThemeSet::load_defaults();
284        assert!(themes.themes.len() > 4);
285    }
286}