Skip to main content

qcode/value/
bytes.rs

1//! Opaque compile-time byte blobs — constants wider than a [`Literal`](crate::value::literal::Literal) can hold.
2//!
3//! A numeric [`Literal`](crate::value::literal::Literal) is a single `u64`; constants
4//! that exceed 64 bits (SSE/AVX register pools, wide stack/memory reads, the
5//! result of coalescing several adjacent constant stores) cannot be represented
6//! that way without breaking the u64-centric folding pipeline. A [`Bytes`]
7//! value sidesteps that: it is a raw, opaque byte vector with **no arithmetic
8//! meaning**, stored as a little-endian, memory-order snapshot (`data[i]` is the
9//! byte at `base + i`, matching the target's fixed little-endian layout).
10//!
11//! Every `Bytes` carries a [`TypeId`] — typically an `Array(i8, len)` — with the
12//! invariant `size_of(type_id) == data.len()`, enforced at construction. Unlike
13//! numeric literals, `Bytes` values are **not interned**: each construction
14//! produces a fresh [`BytesId`], so downstream equality must compare contents,
15//! never IDs.
16
17use crate::{
18    context::Shared,
19    types::TypeId,
20    value::{
21        Value, ValueId,
22        util::base_ref::{BaseRef, WithShared},
23    },
24};
25use jstd::Identifier;
26
27#[derive(Identifier)]
28pub struct BytesId(usize);
29
30/// A compile-time opaque byte blob stored in a [`Context`](crate::context::Context).
31///
32/// The bytes are held in little-endian, memory-order layout. See the module
33/// docs for the rationale and invariants.
34#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
35pub struct Bytes {
36    /// Raw bytes in target memory order (`data[i]` = byte at `base + i`).
37    pub data: Vec<u8>,
38    /// The type of this constant; `size_of(type_id) == data.len()`.
39    pub type_id: TypeId,
40}
41
42pub type BytesRef<'str, 'ctx> = BaseRef<&'ctx Shared<'str>, BytesId>;
43
44impl<'s, 'ctx: 's, 'str: 'ctx> WithShared<'s, 'ctx, 'str> for BytesRef<'str, 'ctx> {
45    fn shared(&'s self) -> &'ctx Shared<'str> {
46        self.ctx
47    }
48}
49
50impl<'s, 'ctx: 's, 'str: 'ctx, Ctx> BaseRef<Ctx, BytesId>
51where
52    Self: WithShared<'s, 'ctx, 'str>,
53{
54    fn inner(&'s self) -> &'ctx Bytes {
55        &self.shared().values.bytes[self.id]
56    }
57
58    /// The raw bytes in target memory order.
59    pub fn data(&'s self) -> &'ctx [u8] {
60        &self.inner().data
61    }
62
63    /// Returns the [`TypeId`] of this blob.
64    pub fn type_id(&'s self) -> TypeId {
65        self.inner().type_id
66    }
67}
68
69/// The encoding under which a byte blob was successfully read as text.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
71pub enum StringEncoding {
72    /// Printable 7-bit ASCII (one byte per character).
73    Ascii,
74    /// Printable UTF-16, little-endian (two bytes per code unit).
75    Utf16Le,
76}
77
78impl StringEncoding {
79    /// Short human-readable label (e.g. for a UI column).
80    pub fn label(self) -> &'static str {
81        match self {
82            StringEncoding::Ascii => "ascii",
83            StringEncoding::Utf16Le => "utf16le",
84        }
85    }
86}
87
88/// Attempt to decode `data` as a printable ASCII or UTF-16LE string.
89///
90/// Both encodings tolerate a single trailing NUL terminator (the common C /
91/// Windows-`W` convention). Returns the decoded text and the encoding it was
92/// read under, but only when every character is printable; otherwise the blob
93/// has no clean string reading and the caller should fall back to the `\xNN`
94/// hex form.
95pub fn decode_string(data: &[u8]) -> Option<(StringEncoding, String)> {
96    if data.is_empty() {
97        return None;
98    }
99
100    // ASCII, optionally NUL-terminated.
101    let ascii = data.strip_suffix(&[0]).unwrap_or(data);
102    if !ascii.is_empty() && ascii.iter().all(|&b| b.is_ascii_graphic() || b == b' ') {
103        return Some((
104            StringEncoding::Ascii,
105            ascii.iter().map(|&b| b as char).collect(),
106        ));
107    }
108
109    // UTF-16LE, optionally NUL-terminated.
110    if data.len() >= 2 && data.len().is_multiple_of(2) {
111        let units: Vec<u16> = data
112            .chunks_exact(2)
113            .map(|c| u16::from_le_bytes([c[0], c[1]]))
114            .collect();
115        let units = units.strip_suffix(&[0]).unwrap_or(&units);
116        // Require printable ASCII-range code units: random binary read as
117        // UTF-16 lands in CJK / presentation-form ranges that decode to valid
118        // but meaningless text (e.g. "凜ﯺ"). Genuine wide strings (the Windows
119        // `W`-API convention, e.g. "ntdll.dll") are ASCII in the low byte with a
120        // zero high byte, so this keeps real strings and drops the noise.
121        if !units.is_empty()
122            && units
123                .iter()
124                .all(|&u| u < 0x80 && (u as u8).is_ascii_graphic() || u == b' ' as u16)
125        {
126            let s: String = units.iter().map(|&u| u as u8 as char).collect();
127            return Some((StringEncoding::Utf16Le, s));
128        }
129    }
130
131    None
132}
133
134/// Escape a decoded string for display inside `b"..."` quotes.
135pub fn escape_decoded(s: &str) -> String {
136    let mut out = String::with_capacity(s.len());
137    for c in s.chars() {
138        match c {
139            '"' | '\\' => {
140                out.push('\\');
141                out.push(c);
142            }
143            _ => out.push(c),
144        }
145    }
146    out
147}
148
149/// How a [`Bytes`] blob should be rendered as a `b"..."` literal.
150///
151/// [`Auto`](BytesDisplay::Auto) is the default and lets [`decode_string`] pick
152/// the encoding (or fall back to hex). The remaining variants are user-forced
153/// overrides — e.g. from the GUI Strings pane — and are applied even when the
154/// blob is not cleanly printable, escaping any bytes that don't fit.
155#[derive(
156    Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
157)]
158pub enum BytesDisplay {
159    /// Auto-detect ASCII / UTF-16LE, else hex.
160    #[default]
161    Auto,
162    /// Force ASCII rendering.
163    Ascii,
164    /// Force UTF-16LE rendering.
165    Utf16Le,
166    /// Force raw `\xNN` hex.
167    Raw,
168}
169
170/// Push one ASCII byte to `out`, either as a literal char or a `\xNN` escape.
171fn push_ascii_byte(out: &mut String, b: u8) {
172    match b {
173        b'"' | b'\\' => {
174            out.push('\\');
175            out.push(b as char);
176        }
177        _ if b.is_ascii_graphic() || b == b' ' => out.push(b as char),
178        _ => out.push_str(&format!("\\x{b:02x}")),
179    }
180}
181
182/// Render `data` as the full `b"..."` literal text under `mode`.
183///
184/// Forced ASCII/UTF-16LE modes are best-effort: bytes (or code units) that
185/// aren't printable are escaped rather than rejected, so the user always sees
186/// the override they asked for.
187pub fn render_bytes_literal(data: &[u8], mode: BytesDisplay) -> String {
188    let mut out = String::new();
189    out.push_str("b\"");
190    match mode {
191        BytesDisplay::Auto => {
192            if let Some((_, s)) = decode_string(data) {
193                out.push_str(&escape_decoded(&s));
194            } else {
195                for &b in data {
196                    out.push_str(&format!("\\x{b:02x}"));
197                }
198            }
199        }
200        BytesDisplay::Ascii => {
201            for &b in data {
202                push_ascii_byte(&mut out, b);
203            }
204        }
205        BytesDisplay::Utf16Le => {
206            let mut chunks = data.chunks_exact(2);
207            for c in &mut chunks {
208                let unit = u16::from_le_bytes([c[0], c[1]]);
209                match char::from_u32(unit as u32) {
210                    Some(ch) if !ch.is_control() => match ch {
211                        '"' | '\\' => {
212                            out.push('\\');
213                            out.push(ch);
214                        }
215                        _ => out.push(ch),
216                    },
217                    _ => out.push_str(&format!("\\u{{{unit:04x}}}")),
218                }
219            }
220            // Trailing odd byte, if any.
221            for &b in chunks.remainder() {
222                out.push_str(&format!("\\x{b:02x}"));
223            }
224        }
225        BytesDisplay::Raw => {
226            for &b in data {
227                out.push_str(&format!("\\x{b:02x}"));
228            }
229        }
230    }
231    out.push('"');
232    out
233}
234
235impl std::fmt::Display for BytesRef<'_, '_> {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        let data = &self.ctx.values.bytes[self.id].data;
238        let mode = self.ctx.bytes_display(self.id);
239        if mode != BytesDisplay::Auto {
240            return f.write_str(&render_bytes_literal(data, mode));
241        }
242        if let Some((_, s)) = decode_string(data) {
243            return write!(f, "b\"{}\"", escape_decoded(&s));
244        }
245        write!(f, "b\"")?;
246        for &b in data {
247            write!(f, "\\x{:02x}", b)?;
248        }
249        write!(f, "\"")
250    }
251}
252
253impl<'str, 'ctx> Value<'str, 'ctx> for BytesRef<'str, 'ctx> {
254    fn id(&self) -> ValueId {
255        ValueId::Bytes(self.id)
256    }
257
258    fn size(&self) -> usize {
259        self.ctx.values.bytes[self.id].data.len()
260    }
261}