Skip to main content

hermes_support/
buffer.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8//! Source buffer primitives: a NUL-terminated byte buffer and a named wrapper.
9
10use std::cell::RefCell;
11use std::io::Read;
12
13use crate::line_index::LineIndex;
14
15/// A null terminated memory buffer.
16#[derive(Debug)]
17pub struct NullTerminatedBuf(Vec<u8>);
18
19impl NullTerminatedBuf {
20    /// Create from a reader and null terminate.
21    pub fn from_reader(reader: &mut dyn Read) -> Result<NullTerminatedBuf, std::io::Error> {
22        let mut v = Vec::<u8>::new();
23        reader.read_to_end(&mut v)?;
24        v.push(0);
25
26        Ok(NullTerminatedBuf(v))
27    }
28
29    /// Create from a file and null terminate it.
30    pub fn from_file(f: &'_ mut std::fs::File) -> Result<NullTerminatedBuf, std::io::Error> {
31        // TODO: this is an extremely naive implementation, it can be optimized in multiple ways:
32        //       - obtain the size of the file and perform a single allocation and few syscalls
33        //       - memory map the file
34        //       - just use LLVM's MemoryBuffer
35        //       One problem is that there isn't an obvious way in Rust to check portably whether
36        //       something has a fixed size and is memory mappable (i.e. is not a pipe).
37
38        Self::from_reader(f)
39    }
40
41    /// Create by copying a slice and appending null-termination.
42    pub fn from_slice_copy(s: &[u8]) -> NullTerminatedBuf {
43        let mut v = Vec::with_capacity(s.len() + 1);
44        v.extend_from_slice(s);
45        v.push(0);
46        NullTerminatedBuf(v)
47    }
48
49    /// Create from a slice that may already be null-terminated.
50    pub fn from_slice_check(s: &[u8]) -> NullTerminatedBuf {
51        Self::from_slice_copy(if let [head @ .., 0] = s { head } else { s })
52    }
53
54    /// Create by copying a string and appending null-termination.
55    pub fn from_str_copy(s: &str) -> NullTerminatedBuf {
56        Self::from_slice_copy(s.as_bytes())
57    }
58
59    /// Create from a string that may already be null-terminated.
60    pub fn from_str_check(s: &str) -> NullTerminatedBuf {
61        Self::from_slice_check(s.as_bytes())
62    }
63
64    /// Return the length of the data including the null terminator.
65    pub fn len(&self) -> usize {
66        self.0.len()
67    }
68
69    /// Just a placeholder always returning `true`, since the there is always
70    /// at least a null terminator.
71    pub fn is_empty(&self) -> bool {
72        false
73    }
74
75    pub fn as_bytes(&self) -> &[u8] {
76        self.0.as_slice()
77    }
78}
79
80impl AsRef<[u8]> for NullTerminatedBuf {
81    fn as_ref(&self) -> &[u8] {
82        self.as_bytes()
83    }
84}
85
86/// A named source buffer: its file name, its NUL-terminated bytes, and a lazily
87/// built line index. This is the Rust analog of an `llvh::MemoryBuffer`
88/// registered in a `SourceMgr`, but it carries its own name.
89pub struct SourceBuffer {
90    name: String,
91    buf: NullTerminatedBuf,
92    /// Lazily built on first line/col resolution. Interior mutability so that
93    /// resolution can happen through a shared `&SourceBuffer`.
94    line_index: RefCell<Option<LineIndex>>,
95}
96
97impl SourceBuffer {
98    pub fn from_str(name: impl Into<String>, contents: &str) -> SourceBuffer {
99        SourceBuffer {
100            name: name.into(),
101            buf: NullTerminatedBuf::from_str_copy(contents),
102            line_index: RefCell::new(None),
103        }
104    }
105
106    pub fn from_slice_check(name: impl Into<String>, contents: &[u8]) -> SourceBuffer {
107        SourceBuffer {
108            name: name.into(),
109            buf: NullTerminatedBuf::from_slice_check(contents),
110            line_index: RefCell::new(None),
111        }
112    }
113
114    pub fn name(&self) -> &str {
115        &self.name
116    }
117
118    /// The source bytes, excluding the trailing NUL terminator.
119    pub fn bytes(&self) -> &[u8] {
120        let raw = self.buf.as_bytes();
121        &raw[..raw.len() - 1]
122    }
123
124    /// The raw bytes, including the trailing NUL terminator.
125    pub fn raw(&self) -> &[u8] {
126        self.buf.as_bytes()
127    }
128
129    /// Run `f` with this buffer's line index, building and caching it on first use.
130    pub fn with_line_index<R>(&self, f: impl FnOnce(&LineIndex, &[u8]) -> R) -> R {
131        {
132            let mut slot = self.line_index.borrow_mut();
133            if slot.is_none() {
134                *slot = Some(LineIndex::build(self.bytes()));
135            }
136        }
137        let slot = self.line_index.borrow();
138        f(slot.as_ref().unwrap(), self.bytes())
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn null_terminated_and_named() {
148        let b = SourceBuffer::from_str("foo.js", "abc");
149        assert_eq!(b.name(), "foo.js");
150        // bytes() excludes the terminator; raw includes it.
151        assert_eq!(b.bytes(), b"abc");
152        assert_eq!(b.raw()[b.raw().len() - 1], 0u8);
153    }
154
155    #[test]
156    fn already_terminated_input_not_doubled() {
157        let b = SourceBuffer::from_slice_check("x", b"ab\0");
158        assert_eq!(b.bytes(), b"ab");
159    }
160
161    #[test]
162    fn empty_input_gives_empty_bytes() {
163        // Edge case: the only construction where raw().len() - 1 == 0.
164        let b = SourceBuffer::from_str("empty.js", "");
165        assert_eq!(b.bytes(), b"");
166        assert_eq!(b.raw(), b"\0");
167        assert_eq!(b.raw().len(), b.bytes().len() + 1);
168    }
169}