Skip to main content

documents/
window.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! How much of a version one answer carries, and where it is cut
7//! (`crates/documents/spec/Window.lean`, D7).
8//!
9//! A window never splits a character, never runs past the version, and
10//! never carries more than [`WINDOW_BYTES_MAX`] bytes past the offset it
11//! was asked from. A cut moves backwards to the nearest character
12//! boundary rather than forwards, so a window is never longer than it
13//! was allowed to be; a start inside a character moves back to that
14//! character's first byte, so a reader that asked from the middle of one
15//! is given the whole of it.
16
17use kernel::{AxCode, AxError};
18use serde::{Deserialize, Serialize};
19
20use crate::encoding::Encoding;
21use crate::format::Format;
22use crate::layout;
23use crate::span::{Span, offset};
24
25/// The most bytes one window carries past the offset it was asked from.
26///
27/// The bound the document answer had before it carried versions, kept so
28/// a page's first answer costs what it cost; the reading that would move
29/// it is the first-viewport time over a fixed corpus (refrain roadmap
30/// A11), which no machine has taken yet.
31pub const WINDOW_BYTES_MAX: u64 = 64 * 1024;
32
33/// How far a boundary can lie behind a byte in any encoding read here:
34/// three continuation bytes in UTF-8, one odd byte and a low surrogate in
35/// UTF-16.
36const REACH: u64 = 3;
37
38/// One stretch of a version, and the characters it spells.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
41pub struct Window {
42    pub span: Span,
43    pub text: String,
44}
45
46/// Bytes lifted out of a version so a window can be cut from them.
47#[derive(Debug, Clone, Copy)]
48pub struct Lifted<'a> {
49    /// The offset of `bytes[0]` in the version.
50    pub at: u64,
51    pub bytes: &'a [u8],
52    /// The length of the whole version.
53    pub size: u64,
54}
55
56/// The first window of a whole version.
57///
58/// A version that fits is given whole. One that does not ends at the
59/// last block that fits, so the first screen a reader sees holds no half
60/// paragraph; when not even the first block fits, at the last character
61/// boundary that does. Blocks are read only in the UTF-8 encodings,
62/// whose line endings are the bytes the layout looks for.
63///
64/// # Errors
65/// The [`Encoding::decode`] refusal, for bytes that are not text in
66/// `encoding`.
67pub fn head(format: Format, encoding: Encoding, source: &[u8]) -> Result<Window, AxError> {
68    let size = offset(source.len());
69    let fits = match encoding {
70        Encoding::Utf8 | Encoding::Utf8Bom if size > WINDOW_BYTES_MAX => {
71            layout::blocks(format, source)
72                .iter()
73                .map(|block| block.end())
74                .take_while(|end| *end <= WINDOW_BYTES_MAX)
75                .last()
76                .filter(|end| *end > 0)
77        }
78        Encoding::Utf8 | Encoding::Utf8Bom | Encoding::Utf16Le | Encoding::Utf16Be => None,
79    };
80    let wanted = Span::ordered(0, fits.unwrap_or(size));
81    cut(
82        encoding,
83        Lifted {
84            at: 0,
85            bytes: source,
86            size,
87        },
88        wanted,
89    )
90}
91
92/// The bytes to lift out of a version of `size` bytes to cut the window
93/// `wanted` asks for: up to [`REACH`] bytes before it, so a start inside
94/// a character can step back to that character, and up to [`REACH`]
95/// after its bounded end, so the end can tell whether a character runs
96/// on past it.
97pub fn lift(wanted: Span, size: u64) -> Span {
98    let start = wanted.start().min(size);
99    Span::ordered(
100        start.saturating_sub(REACH),
101        bounded_end(wanted, size).saturating_add(REACH).min(size),
102    )
103}
104
105/// The window `wanted` asks for, cut from bytes [`lift`] named.
106///
107/// # Errors
108/// The [`Encoding::decode`] refusal, for bytes that are not text in
109/// `encoding` - which is also what a cut that found no boundary inside
110/// the lifted bytes decodes to - and `E_INVALID_ARGS` when `lifted`
111/// does not hold the bytes the window needs.
112pub fn cut(encoding: Encoding, lifted: Lifted<'_>, wanted: Span) -> Result<Window, AxError> {
113    let start = lifted.boundary_at_or_before(encoding, wanted.start().min(lifted.size));
114    let end = lifted
115        .boundary_at_or_before(encoding, bounded_end(wanted, lifted.size))
116        .max(start);
117    let span = Span::ordered(start, end);
118    let bytes = lifted.slice(span).ok_or_else(|| {
119        AxError::failure(
120            AxCode::InvalidArgs,
121            "read a document window",
122            format!(
123                "{}..{} lies outside the bytes lifted from {}",
124                span.start(),
125                span.end(),
126                lifted.at
127            ),
128        )
129        .with_recovery("lift the bytes `lift` names for this window, then cut again")
130    })?;
131    let text = encoding.decode(bytes)?;
132    Ok(Window { span, text })
133}
134
135/// Where a window asked for as `wanted` may end at most: inside the
136/// version, and no more than [`WINDOW_BYTES_MAX`] past its start.
137fn bounded_end(wanted: Span, size: u64) -> u64 {
138    let start = wanted.start().min(size);
139    wanted
140        .end()
141        .min(size)
142        .min(start.saturating_add(WINDOW_BYTES_MAX))
143        .max(start)
144}
145
146impl Lifted<'_> {
147    /// The nearest character boundary at or before `at`, never stepping
148    /// below the first lifted byte. The version's two ends are
149    /// boundaries whatever bytes sit beside them.
150    fn boundary_at_or_before(&self, encoding: Encoding, at: u64) -> u64 {
151        let floor = self.at.min(at);
152        let mut probe = at;
153        while probe > floor && !self.is_boundary(encoding, probe) {
154            probe = probe.saturating_sub(1);
155        }
156        probe
157    }
158
159    fn is_boundary(&self, encoding: Encoding, at: u64) -> bool {
160        if at == 0 || at >= self.size {
161            return true;
162        }
163        let index = at
164            .checked_sub(self.at)
165            .and_then(|index| usize::try_from(index).ok());
166        index.is_some_and(|index| encoding.begins_at(self.bytes, index, self.at))
167    }
168
169    fn slice(&self, span: Span) -> Option<&[u8]> {
170        let from = span.start().checked_sub(self.at)?;
171        let to = span.end().checked_sub(self.at)?;
172        let (from, to) = Span::ordered(from, to).within(self.bytes.len())?;
173        self.bytes.get(from..to)
174    }
175}
176
177#[cfg(test)]
178#[allow(
179    clippy::unwrap_used,
180    clippy::expect_used,
181    clippy::panic,
182    clippy::indexing_slicing,
183    reason = "test code"
184)]
185mod tests;