nix_index/frcode.rs
1//! A compact encoding for file tree entries based on sharing prefixes.
2//!
3//! This module contains a rust implementation of a variant of the `frcode` tool
4//! used by GNU findutils' locate. It has been extended to allow meta information
5//! to be attached to each entry so it is no longer compatible with the original
6//! frcode format.
7//! (See http://www.delorie.com/gnu/docs/findutils/locatedb.5.html for a description of the frcode format.)
8//!
9//! The basic building block of the encoding is a line. Each line has the following format:
10//! (the spaces are for readability only, they are not present in the encoding)
11//!
12//! ```text
13//! <metadata> <\x00 byte> <shared prefix differential> <additional path bytes> <newline character>
14//! ```
15//!
16//! Each entry holds two parts of data: metadata, which is just some arbitrary blob of NUL-terminated bytes
17//! and a path. Because we are storing file trees, the path will likely share a long prefix with the previous
18//! entry's path (we traverse directory entries in sorted order to maximize this chance), so we first store
19//! the length of the shared prefix.
20//!
21//! Since this length will likely be similar to the previous one (if there are many entries in `/foo/bar`, then they will
22//! all share a prefix of at least the length of `/foo/bar`) we only store the signed *difference* to the previous shared prefix length
23//! (This is why it's called a differential). For differences smaller than +/-127 we store them directly as a single byte. If the
24//! difference is greater than that, the first byte will by `0x80` (-128) indicating that the following two bytes represent the
25//! difference (with the high byte first [big endian]).
26//!
27//! As an example, consider the following non-encoded plaintext, where `:` separates the metadata from the path:
28//!
29//! ```text
30//! d:/
31//! d:/foo
32//! d:/foo/bar
33//! f:/foo/bar/test.txt
34//! f:/foo/bar/text.txt
35//! d:/foo/baz
36//! ```
37//!
38//! This text would be encoded as (using `[v]` to indicate a byte with the value of v)
39//!
40//! ```text
41//! d[0][0]/
42//! d[0][1]foo
43//! d[0][3]/bar
44//! f[0][4]/test.txt
45//! f[0][3]xt.txt
46//! d[0][-4]z
47//! ```
48//!
49//! At the beginning, there is no previous entry, so the shared prefix length must always be `0` (and so must the shared prefix differential).
50//! The second entry shares `1` byte with the first path so the difference is `1`. The third entry shares `4` bytes with the second one, which
51//! is `3` more than the shared length of the second one, so we encode a `3` followed by the non-shared bytes, and so on for the remaining entries.
52//! The last entry shares four bytes less than the second to last one did with its predecessor, so here the differential is negative.
53//!
54//! Through this encoding, the size of the index is typically reduces by a factor of 3 to 5.
55use std::cmp;
56use std::io::{self, BufRead, Write};
57use std::ops::{Deref, DerefMut};
58
59use memchr;
60use thiserror::Error;
61
62#[derive(Error, Debug)]
63pub enum Error {
64 #[error("I/O error: {0}")]
65 Io(#[from] io::Error),
66 #[error("length of shared prefix must be >= 0 and <= {previous_len} (length of previous item), but found: {shared_len}")]
67 SharedOutOfRange {
68 previous_len: usize,
69 shared_len: isize,
70 },
71 #[error("length of shared prefix too big: cannot add {shared_len} to {diff} without overflow")]
72 SharedOverflow { shared_len: isize, diff: isize },
73 #[error("missing terminating NUL byte for entry")]
74 MissingNul,
75 #[error("missing newline separator for entry")]
76 MissingNewline,
77 #[error("missing the shared prefix length differential for entry")]
78 MissingPrefixDifferential,
79}
80
81type Result<T> = std::result::Result<T, Error>;
82
83/// A buffer that may be resizable or not. This is used for decoding,
84/// where we want to make the buffer resizable as long as we haven't decoded
85/// a full entry yet but want to lock it as soon as we got a full entry.
86///
87/// This is necessary because we always need to be able to decode at least
88/// one entry to make progress, as we never return partial entries during decoding.
89struct ResizableBuf {
90 allow_resize: bool,
91 data: Vec<u8>,
92}
93
94impl ResizableBuf {
95 /// Allocates a new resizable buffer with the given initial size.
96 ///
97 /// The new buffer will allow resizing initially.
98 fn new(capacity: usize) -> ResizableBuf {
99 ResizableBuf {
100 data: vec![0; capacity],
101 allow_resize: true,
102 }
103 }
104
105 /// Resizes the buffer to hold at least `new_size` elements. Returns `true`
106 /// if resizing was successful (so that buffer can now hold at least `new_size` elements)
107 /// or `false` if not (meaning `new_size` is greater than the current size and resizing
108 /// was not allowed).
109 fn resize(&mut self, new_size: usize) -> bool {
110 if new_size <= self.data.len() {
111 return true;
112 }
113
114 if !self.allow_resize {
115 return false;
116 }
117
118 self.data.resize(new_size, b'\x00');
119 true
120 }
121}
122
123impl Deref for ResizableBuf {
124 type Target = [u8];
125
126 fn deref(&self) -> &[u8] {
127 &self.data
128 }
129}
130
131impl DerefMut for ResizableBuf {
132 fn deref_mut(&mut self) -> &mut [u8] {
133 &mut self.data
134 }
135}
136
137/// A decoder for the frcode format. It reads data from some input source
138/// and returns blocks of decoded entries.
139///
140/// It will not split the metadata/path parts of individual entries since
141/// the primary use case for this is searching, where it is enough to decode
142/// the entries that match.
143pub struct Decoder<R> {
144 /// The input source from which we decode
145 reader: R,
146 /// Position of the first byte of the path part of the last entry.
147 /// We need this to copy the shared prefix.
148 last_path: usize,
149 /// Position of the start of the entry that didn't fully fit in the buffer in the
150 /// last decode iteration. Since this entry was partial, it hasn't been returned to
151 /// the user yet and we need to continue decoding this entry in this iteration.
152 partial_entry_start: usize,
153 /// The length of the shared prefix for the current entry. This is necessary because
154 /// the shared length is stored as a difference, so we need the previous value to update it.
155 shared_len: isize,
156 /// The buffer into which we store the decoded bytes.
157 buf: ResizableBuf,
158 /// Current write position in buf. The next decoded byte should be written to buf[pos].
159 pos: usize,
160}
161
162impl<R: BufRead> Decoder<R> {
163 /// Construct a new decoder for the given source.
164 pub fn new(reader: R) -> Decoder<R> {
165 let capacity = 1_000_000;
166 Decoder {
167 reader,
168 buf: ResizableBuf::new(capacity),
169 pos: 0,
170 last_path: 0,
171 shared_len: 0,
172 partial_entry_start: 0,
173 }
174 }
175
176 /// Copies `self.shared_len` bytes from the previous entry's path into the output buffer.
177 ///
178 /// Returns false if the buffer was too small and could not be resized. In this case, no
179 /// bytes will be copied.
180 fn copy_shared(&mut self) -> Result<bool> {
181 let shared_len = self.shared_len as usize;
182 let new_pos = self.pos + shared_len;
183 let new_last_path = self.pos;
184 if !self.buf.resize(new_pos) {
185 return Ok(false);
186 }
187
188 if self.shared_len < 0 || self.last_path + shared_len > self.pos {
189 return Err(Error::SharedOutOfRange {
190 previous_len: self.pos - self.last_path,
191 shared_len: self.shared_len,
192 });
193 }
194
195 let (_, last) = self.buf.split_at_mut(self.last_path);
196 let (last, new) = last.split_at_mut(self.pos - self.last_path);
197 new[..shared_len].copy_from_slice(&last[..shared_len]);
198
199 self.pos += shared_len;
200 self.last_path = new_last_path;
201 Ok(true)
202 }
203
204 /// Copies bytes from the input reader to the output buffer until a `\x00` byte is read.
205 /// The NUL byte is included in the output buffer.
206 ///
207 /// Returns false if the output buffer was exhausted before a NUL byte could be found and
208 /// could not be resized. All bytes that were read before this situation was detected will
209 /// have already been copied to the output buffer in this case.
210 ///
211 /// It will also return false if the end of the input was reached.
212 fn read_to_nul(&mut self) -> Result<bool> {
213 loop {
214 let (done, len) = {
215 let &mut Decoder {
216 ref mut reader,
217 ref mut buf,
218 ref mut pos,
219 ..
220 } = self;
221 let input = match reader.fill_buf() {
222 Ok(data) => data,
223 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
224 Err(e) => return Err(Error::from(e)),
225 };
226
227 if input.is_empty() {
228 return Ok(false);
229 }
230
231 let (done, len) = match memchr::memchr(b'\x00', input) {
232 Some(i) => (true, i + 1),
233 None => (false, input.len()),
234 };
235
236 let new_pos = *pos + len;
237 if buf.resize(new_pos) {
238 buf[*pos..new_pos].copy_from_slice(&input[..len]);
239 *pos = new_pos;
240 (done, len)
241 } else {
242 return Ok(false);
243 }
244 };
245 self.reader.consume(len);
246 if done {
247 return Ok(true);
248 }
249 }
250 }
251
252 /// Read the differential from the input reader. This function will return an error
253 /// if the end of input has been reached.
254 fn decode_prefix_diff(&mut self) -> Result<i16> {
255 let mut buf = [0; 1];
256 self.reader
257 .read_exact(&mut buf)
258 .map_err(|_| Error::MissingPrefixDifferential)?;
259
260 if buf[0] != 0x80 {
261 Ok((buf[0] as i8) as i16)
262 } else {
263 let mut buf = [0; 2];
264 self.reader
265 .read_exact(&mut buf)
266 .map_err(|_| Error::MissingPrefixDifferential)?;
267 let high = buf[0] as i16;
268 let low = buf[1] as i16;
269 Ok(high << 8 | low)
270 }
271 }
272
273 /// Decodes some entries to fill the buffer and returns a block of decoded entries.
274 ///
275 /// It will decode as many entries as fit into the internal buffer, but at least one.
276 /// In the returned block of bytes, an entry's metadata and path will be separated by a NUL byte
277 /// and entries will be terminated with a newline character. This allows for fast searching with
278 /// a line based searcher.
279 ///
280 /// The function does not return partially decoded entries. Because of this, the size of returned
281 /// slice will vary from call to call. The last entry which did not fully fit into the buffer yet
282 /// will be returned as the first entry at the next call.
283 pub fn decode(&mut self) -> Result<&mut [u8]> {
284 // Save end pointer from previous iteration and reset write position
285 let end = self.pos;
286 self.pos = 0;
287
288 // We need to preserve some data from the previous iteration, namely:
289 //
290 // * all data after the `self.last_path` position, for copying the shared prefix
291 // * everything from the start of the partial entry, since this entry wasn't fully decoded
292 // in the last iteration and we want to continue decoding it now
293 //
294 // If we stopped decoding the partial entry after already copying the shared prefix, then
295 // `last_path` will already point to the partial entry so it will be greater than `partial_entry_start`.
296 //
297 // If we stopped decoding during copying the metadata though, which comes before we copy the shared
298 // prefix, then `last_path` will point to the previous entry's path, so it will be smaller than
299 // `partial_entry_start`.
300 //
301 // To support both these cases, we take the minimum here.
302 let mut copy_pos = cmp::min(self.partial_entry_start, self.last_path);
303
304 // Since we sometimes copy more than just the partial entry, we need to know where the partial entry
305 // starts as that is the first position that we want to return (everything before that was already
306 // part of an entry returned in the last iteration).
307 let item_start = self.partial_entry_start - copy_pos;
308
309 // Shift the last path, because we copy it from copy_pos.. to 0..
310 self.last_path -= copy_pos;
311
312 // Now we can do the actual copying. We cannot use copy_from_slice here since source and target
313 // may overlap.
314 while copy_pos < end {
315 self.buf[self.pos] = self.buf[copy_pos];
316 self.pos += 1;
317 copy_pos += 1;
318 }
319
320 // Allow resizing the buffer, since we haven't decoded a full entry yet
321 self.buf.allow_resize = true;
322
323 // If the the last decoded byte in the buffer is a NUL byte, that means that
324 // we are now at the start of the path part of the entry. This means that
325 // we need to copy the shared prefix now.
326 let mut found_nul = self.pos > 0 && self.buf[self.pos - 1] == b'\x00';
327 if found_nul {
328 self.copy_shared()?;
329 }
330
331 // At this point, we are guaranteed to be in either the metadata part or the non-shared part
332 // of an entry. In both cases, the action that we need to take is the same: copy data till
333 // the next NUL byte. After the NUL byte, we know that we are at the end of the metadata part,
334 // so we read a differential and copy the shared prefix, and repeat.
335 //
336 // Note that this loop doesn't care about where entries end. Only the path part of each entry requires
337 // special processing, so we can jump from NUL byte to NUL byte, decode the path and then just copy
338 // the data from the source when jumping to the next NUL byte.
339 loop {
340 // Read data up to the next nul byte.
341 if !self.read_to_nul()? {
342 break;
343 }
344
345 // If we have already found a NUL byte before this, so we've now got two NUL bytes, so
346 // we've got at least one full entry in between.
347 self.buf.allow_resize = !found_nul;
348
349 // We found a NUL byte. Note that we need to set this *after* updating allow_resize,
350 // since allow_resize should be set to false only after we've found two NUL bytes.
351 found_nul = true;
352
353 // Parse the next prefix length difference
354 let diff = self.decode_prefix_diff()? as isize;
355
356 // Update the shared len
357 self.shared_len = self
358 .shared_len
359 .checked_add(diff)
360 .ok_or(Error::SharedOverflow {
361 shared_len: self.shared_len,
362 diff,
363 })?;
364
365 // Copy the shared prefix
366 if !self.copy_shared()? {
367 break;
368 }
369 }
370
371 // Since we don't want to return partially decoded items, we need to find the end of the last entry.
372 self.partial_entry_start =
373 memchr::memrchr(b'\n', &self.buf[..self.pos]).ok_or(Error::MissingNewline)? + 1;
374 Ok(&mut self.buf[item_start..self.partial_entry_start])
375 }
376}
377
378/// This struct implements an encoder for the frcode format. The encoder
379/// writes directly to the underlying `Write` instance.
380///
381/// To encode an entry you should first call `write_meta` a number of times
382/// to fill the meta data portion. Then, call `write_path` once to finialize the entry.
383///
384/// One important property of this encoder is that it is safe to open and close
385/// it multiple times on the same stream, like this:
386///
387/// ```text
388/// {
389/// let encoder1 = Encoder::new(&mut stream);
390/// } // encoder1 gets dropped here
391/// {
392/// let encoder2 = Encoder::new(&mut stream);
393/// }
394/// ```
395///
396/// To support this, the encoder has a "footer" item that will get written when it is dropped.
397/// This is necessary because we need to write at least one more entry to reset the shared prefix
398/// length to zero, since the next encoder will expect that as initial state.
399pub struct Encoder<W: Write> {
400 writer: W,
401 last: Vec<u8>,
402 shared_len: i16,
403 footer_meta: Vec<u8>,
404 footer_path: Vec<u8>,
405 footer_written: bool,
406}
407
408impl<W: Write> Drop for Encoder<W> {
409 fn drop(&mut self) {
410 self.write_footer().expect("failed to write footer")
411 }
412}
413
414impl<W: Write> Encoder<W> {
415 /// Constructs a new encoder for the specific writer.
416 ///
417 /// The encoder will write the given `footer_meta` and `footer_path` as the last entry.
418 ///
419 /// # Panics
420 ///
421 /// If either `footer_meta` or `footer_path` contain NUL or newline bytes.
422 pub fn new(writer: W, footer_meta: Vec<u8>, footer_path: Vec<u8>) -> Encoder<W> {
423 assert!(
424 !footer_meta.contains(&b'\x00'),
425 "footer meta must not contain null bytes"
426 );
427 assert!(
428 !footer_path.contains(&b'\x00'),
429 "footer path must not contain null bytes"
430 );
431 assert!(
432 !footer_meta.contains(&b'\n'),
433 "footer meta must not contain newlines"
434 );
435 assert!(
436 !footer_path.contains(&b'\n'),
437 "footer path must not contain newlines"
438 );
439 Encoder {
440 writer,
441 last: Vec::new(),
442 shared_len: 0,
443 footer_meta,
444 footer_path,
445 footer_written: false,
446 }
447 }
448
449 /// Writes the specific shared prefix differential to the output stream.
450 ///
451 /// This function takes care of the variable-length encoding using for prefix differentials
452 /// in the frcode format.
453 fn encode_diff(&mut self, diff: i16) -> io::Result<()> {
454 let low = (diff & 0xFF) as u8;
455 if diff.abs() < i8::MAX as i16 {
456 self.writer.write_all(&[low])?;
457 } else {
458 let high = ((diff >> 8) & 0xFF) as u8;
459 self.writer.write_all(&[0x80, high, low])?;
460 }
461 Ok(())
462 }
463
464 /// Writes the meta data of an entry to the output stream.
465 ///
466 /// This function can be called multiple times to extend the current meta data part.
467 /// Since the meta data is written as-is to the output stream, calling the function
468 /// multiple times will concatenate the meta data of all calls.
469 ///
470 /// # Panics
471 ///
472 /// If the meta data contains NUL bytes or newlines.
473 pub fn write_meta(&mut self, meta: &[u8]) -> io::Result<()> {
474 assert!(
475 !meta.contains(&b'\x00'),
476 "entry must not contain null bytes"
477 );
478 assert!(!meta.contains(&b'\n'), "entry must not contain newlines");
479
480 self.writer.write_all(meta)?;
481 Ok(())
482 }
483
484 /// Finalizes an entry by encoding its path to the output stream.
485 ///
486 /// This function should be called after you've finished writing the meta data for
487 /// the current entry. It will terminate the meta data part by writing the NUL byte
488 /// and then encode the path into the output stream.
489 ///
490 /// The entry will be terminated with a newline.
491 ///
492 /// # Panics
493 ///
494 /// If the path contains NUL bytes or newlines.
495 pub fn write_path(&mut self, path: Vec<u8>) -> io::Result<()> {
496 assert!(
497 !path.contains(&b'\x00'),
498 "entry must not contain null bytes"
499 );
500 assert!(!path.contains(&b'\x00'), "entry must not contain newlines");
501 self.writer.write_all(b"\x00")?;
502
503 let mut shared: isize = 0;
504 let max_shared = i16::MAX as isize;
505 for (a, b) in self.last.iter().zip(path.iter()) {
506 if a != b || shared > max_shared {
507 break;
508 }
509 shared += 1;
510 }
511 let shared = shared as i16;
512
513 let diff = shared - self.shared_len;
514 self.encode_diff(diff)?;
515
516 self.last = path;
517 self.shared_len = shared;
518
519 let pos = shared as usize;
520 self.writer.write_all(&self.last[pos..])?;
521 self.writer.write_all(b"\n")?;
522
523 Ok(())
524 }
525
526 /// Writes the footer entry.
527 ///
528 /// The footer entry will not share any prefix with the preceding entry,
529 /// so after this function, the shared prefix length is zero. This guarantees
530 /// that we can start another Encoder after this item, since the Encoder expects
531 /// the initial shared prefix length to be zero.
532 fn write_footer(&mut self) -> io::Result<()> {
533 if self.footer_written {
534 return Ok(());
535 }
536
537 let diff = -self.shared_len;
538 self.writer.write_all(&self.footer_meta)?;
539 self.writer.write_all(b"\x00")?;
540 self.encode_diff(diff)?;
541 self.writer.write_all(&self.footer_path)?;
542 self.writer.write_all(b"\n")?;
543 self.footer_written = true;
544 Ok(())
545 }
546
547 /// Finishes the encoder by writing the footer entry.
548 ///
549 /// This function is called by drop, but calling it explictly is recommended as
550 /// drop has no way to report IO errors that may occur during writing the footer.
551 pub fn finish(mut self) -> io::Result<()> {
552 self.write_footer()?;
553
554 Ok(())
555 }
556}