Skip to main content

cuttlefish_host/
handles.rs

1//! Files the host holds open on a guest's behalf.
2//!
3//! This is the host side of the rule that bulk data never enters guest memory. A
4//! block receives a handle and a length, then pulls bounded windows; the host
5//! seeks and reads each window straight off disk. Neither side ever holds the
6//! whole file, so guest memory tracks the window size a block chose rather than
7//! the size of its input.
8
9use cuttlefish_abi::MediaKind;
10use std::collections::HashMap;
11use std::fs::File;
12use std::io::{Read, Seek, SeekFrom};
13use std::path::Path;
14
15/// What a handle refers to.
16///
17/// Rendered pages have no file behind them, so a handle is either something on
18/// disk or bytes the host produced. Both answer the same commands, which is what
19/// lets a rendered page be used anywhere a file-backed image can.
20enum Source {
21    /// Read from disk on demand, so a large file is never resident.
22    File(File),
23    /// Held in memory — a rasterized page, which exists nowhere else.
24    Memory(Vec<u8>),
25}
26
27/// One thing held open for a job.
28struct OpenFile {
29    source: Source,
30    len: u64,
31    kind: MediaKind,
32}
33
34/// A job's open files.
35///
36/// Scoping this to a single job is a security property rather than tidiness.
37/// Because the table lives and dies with one job, a handle from another job
38/// names nothing here — which is what lets [`Handles::slice`] skip a capability
39/// check entirely. The check happened once, at [`Handles::open`], and a handle
40/// cannot be forged into a reference to someone else's data.
41#[derive(Default)]
42pub struct Handles {
43    next: u32,
44    open: HashMap<u32, OpenFile>,
45}
46
47/// Why a handle operation failed.
48#[derive(Debug, thiserror::Error)]
49pub enum HandleError {
50    /// The handle does not belong to this job, or never existed.
51    #[error("no such handle: {0}")]
52    BadHandle(u32),
53    /// The requested offset is beyond the end of the file.
54    #[error("offset {offset} is past end of file ({len} bytes)")]
55    OffsetPastEnd {
56        /// The offset that was asked for.
57        offset: u64,
58        /// The file's actual length.
59        len: u64,
60    },
61    /// The window was too small to contain even one whole character.
62    #[error("window of {0} bytes is too small to hold one character")]
63    WindowTooSmall(u64),
64    /// Underlying I/O failure.
65    #[error(transparent)]
66    Io(#[from] std::io::Error),
67}
68
69/// One window of a file.
70pub struct Window {
71    /// The window's contents.
72    pub text: String,
73    /// Where the returned text actually ended; see [`Handles::slice`].
74    pub next_offset: u64,
75}
76
77impl Handles {
78    /// Open a file, returning its handle, length, and what the host made of it.
79    ///
80    /// The caller is responsible for having capability-checked `path` first —
81    /// this type deliberately knows nothing about capabilities, so that the
82    /// check lives in exactly one place rather than being half-enforced here.
83    pub fn open(&mut self, path: &Path) -> Result<(u32, u64, MediaKind), HandleError> {
84        let mut file = File::open(path)?;
85        let len = file.metadata()?.len();
86
87        // Sniff content rather than trusting the extension: a `.txt` holding a
88        // PNG is a file a block should be told about, and an extension is
89        // whatever the last program to touch the file decided.
90        let mut head = vec![0u8; 4096.min(len as usize)];
91        file.read_exact(&mut head)?;
92        file.seek(SeekFrom::Start(0))?;
93        let kind = classify(&head, len);
94
95        let handle = self.next;
96        self.next += 1;
97        self.open.insert(
98            handle,
99            OpenFile {
100                source: Source::File(file),
101                len,
102                kind: kind.clone(),
103            },
104        );
105        Ok((handle, len, kind))
106    }
107
108    /// Register bytes the host produced — a rendered page — as a new handle.
109    pub fn insert_bytes(&mut self, bytes: Vec<u8>, kind: MediaKind) -> (u32, u64) {
110        let len = bytes.len() as u64;
111        let handle = self.next;
112        self.next += 1;
113        self.open.insert(
114            handle,
115            OpenFile {
116                source: Source::Memory(bytes),
117                len,
118                kind,
119            },
120        );
121        (handle, len)
122    }
123
124    /// What a handle refers to.
125    pub fn kind(&self, handle: u32) -> Result<MediaKind, HandleError> {
126        self.open
127            .get(&handle)
128            .map(|f| f.kind.clone())
129            .ok_or(HandleError::BadHandle(handle))
130    }
131
132    /// Every byte behind a handle.
133    ///
134    /// Used for images headed to a vision model, where the whole thing has to be
135    /// sent. Deliberately host-side only — this is the one place a whole file is
136    /// materialized, and it never crosses into guest memory.
137    pub fn read_all(&mut self, handle: u32) -> Result<Vec<u8>, HandleError> {
138        let f = self
139            .open
140            .get_mut(&handle)
141            .ok_or(HandleError::BadHandle(handle))?;
142        match &mut f.source {
143            Source::Memory(bytes) => Ok(bytes.clone()),
144            Source::File(file) => {
145                let mut buf = Vec::with_capacity(f.len as usize);
146                file.seek(SeekFrom::Start(0))?;
147                file.read_to_end(&mut buf)?;
148                Ok(buf)
149            }
150        }
151    }
152
153    /// Read one window as raw bytes, with no character-boundary handling.
154    pub fn slice_bytes(
155        &mut self,
156        handle: u32,
157        offset: u64,
158        len: u64,
159    ) -> Result<(Vec<u8>, u64), HandleError> {
160        let f = self
161            .open
162            .get_mut(&handle)
163            .ok_or(HandleError::BadHandle(handle))?;
164        if offset > f.len {
165            return Err(HandleError::OffsetPastEnd { offset, len: f.len });
166        }
167
168        let want = len.min(f.len - offset) as usize;
169        let mut buf = vec![0u8; want];
170        match &mut f.source {
171            Source::Memory(bytes) => {
172                buf.copy_from_slice(&bytes[offset as usize..offset as usize + want]);
173            }
174            Source::File(file) => {
175                file.seek(SeekFrom::Start(offset))?;
176                file.read_exact(&mut buf)?;
177            }
178        }
179        Ok((buf, offset + want as u64))
180    }
181
182    /// Read one window, truncated to a UTF-8 character boundary.
183    ///
184    /// The truncation is the subtle part, and the reason [`Window::next_offset`]
185    /// exists at all. A caller walking a file picks window sizes with no idea
186    /// where characters begin, so a naive read splits a multi-byte character at
187    /// nearly every seam and yields mojibake. Instead the window is cut back to
188    /// the last complete character and `next_offset` reports where that landed —
189    /// so a caller resuming from `next_offset`, rather than advancing by the
190    /// length it requested, never observes a split.
191    ///
192    /// Reading past the end is not an error: the window is clamped, because a
193    /// block asking for a full window at the tail of a file is behaving
194    /// correctly. Starting past the end *is* an error, since that indicates the
195    /// caller has lost track of where it is.
196    pub fn slice(&mut self, handle: u32, offset: u64, len: u64) -> Result<Window, HandleError> {
197        let f = self
198            .open
199            .get_mut(&handle)
200            .ok_or(HandleError::BadHandle(handle))?;
201
202        if offset > f.len {
203            return Err(HandleError::OffsetPastEnd { offset, len: f.len });
204        }
205
206        let want = len.min(f.len - offset) as usize;
207        let mut buf = vec![0u8; want];
208        match &mut f.source {
209            Source::Memory(bytes) => {
210                buf.copy_from_slice(&bytes[offset as usize..offset as usize + want]);
211            }
212            Source::File(file) => {
213                file.seek(SeekFrom::Start(offset))?;
214                file.read_exact(&mut buf)?;
215            }
216        }
217
218        let valid = match std::str::from_utf8(&buf) {
219            Ok(_) => buf.len(),
220            Err(e) => e.valid_up_to(),
221        };
222
223        // A window landing entirely inside one character would otherwise return
224        // empty forever, and a caller looping until it reaches the end would
225        // spin making no progress and reporting no problem. Failing is strictly
226        // better than that silence.
227        if valid == 0 && !buf.is_empty() {
228            return Err(HandleError::WindowTooSmall(len));
229        }
230        buf.truncate(valid);
231
232        Ok(Window {
233            text: String::from_utf8(buf).expect("truncated at a validated boundary"),
234            next_offset: offset + valid as u64,
235        })
236    }
237}
238
239/// Work out what a file holds from its leading bytes.
240///
241/// Magic numbers, not extensions. An extension records whatever the last program
242/// to touch the file believed; the bytes record what it is.
243fn classify(head: &[u8], len: u64) -> MediaKind {
244    if head.starts_with(b"%PDF-") {
245        // Page count and text-layer detection need the whole file, so they are
246        // filled in by the document layer; this is the fallback when it cannot.
247        return MediaKind::Document {
248            pages: 0,
249            has_text_layer: false,
250        };
251    }
252    if head.starts_with(&[0x89, b'P', b'N', b'G']) {
253        return MediaKind::Image {
254            format: "png".into(),
255        };
256    }
257    if head.starts_with(&[0xFF, 0xD8, 0xFF]) {
258        return MediaKind::Image {
259            format: "jpeg".into(),
260        };
261    }
262    if head.starts_with(b"GIF8") {
263        return MediaKind::Image {
264            format: "gif".into(),
265        };
266    }
267    if head.len() >= 12 && head.starts_with(b"RIFF") && &head[8..12] == b"WEBP" {
268        return MediaKind::Image {
269            format: "webp".into(),
270        };
271    }
272
273    // An empty file is text: there is nothing in it to be anything else, and
274    // calling it binary would make a block reach for the wrong commands.
275    if len == 0 {
276        return MediaKind::Text;
277    }
278
279    // Valid UTF-8 in the first window is good evidence of text. It can be wrong
280    // for a binary file that happens to begin with valid UTF-8, which is why
281    // `Slice` still fails cleanly on bytes it cannot decode rather than trusting
282    // this.
283    match std::str::from_utf8(head) {
284        Ok(_) => MediaKind::Text,
285        // A trailing partial character means the window cut a multi-byte
286        // sequence, which is what text looks like — not a reason to call it
287        // binary.
288        Err(e) if e.error_len().is_none() && e.valid_up_to() > 0 => MediaKind::Text,
289        Err(_) => MediaKind::Binary,
290    }
291}