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 (pairs, _) = data.as_chunks::<2>();
112        let units: Vec<u16> = pairs.iter().map(|&pair| u16::from_le_bytes(pair)).collect();
113        let units = units.strip_suffix(&[0]).unwrap_or(&units);
114        // Require printable ASCII-range code units: random binary read as
115        // UTF-16 lands in CJK / presentation-form ranges that decode to valid
116        // but meaningless text (e.g. "凜ﯺ"). Genuine wide strings (the Windows
117        // `W`-API convention, e.g. "ntdll.dll") are ASCII in the low byte with a
118        // zero high byte, so this keeps real strings and drops the noise.
119        if !units.is_empty()
120            && units
121                .iter()
122                .all(|&u| u < 0x80 && (u as u8).is_ascii_graphic() || u == b' ' as u16)
123        {
124            let s: String = units.iter().map(|&u| u as u8 as char).collect();
125            return Some((StringEncoding::Utf16Le, s));
126        }
127    }
128
129    None
130}
131
132/// Escape a decoded string for display inside `b"..."` quotes.
133pub fn escape_decoded(s: &str) -> String {
134    let mut out = String::with_capacity(s.len());
135    for c in s.chars() {
136        match c {
137            '"' | '\\' => {
138                out.push('\\');
139                out.push(c);
140            }
141            _ => out.push(c),
142        }
143    }
144    out
145}
146
147/// How a [`Bytes`] blob should be rendered as a `b"..."` literal.
148///
149/// [`Auto`](BytesDisplay::Auto) is the default and lets [`decode_string`] pick
150/// the encoding (or fall back to hex). The remaining variants are user-forced
151/// overrides — e.g. from the GUI Strings pane — and are applied even when the
152/// blob is not cleanly printable, escaping any bytes that don't fit.
153#[derive(
154    Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
155)]
156pub enum BytesDisplay {
157    /// Auto-detect ASCII / UTF-16LE, else hex.
158    #[default]
159    Auto,
160    /// Force ASCII rendering.
161    Ascii,
162    /// Force UTF-16LE rendering.
163    Utf16Le,
164    /// Force raw `\xNN` hex.
165    Raw,
166}
167
168/// Push one ASCII byte to `out`, either as a literal char or a `\xNN` escape.
169fn push_ascii_byte(out: &mut String, b: u8) {
170    match b {
171        b'"' | b'\\' => {
172            out.push('\\');
173            out.push(b as char);
174        }
175        _ if b.is_ascii_graphic() || b == b' ' => out.push(b as char),
176        _ => out.push_str(&format!("\\x{b:02x}")),
177    }
178}
179
180/// Render `data` as the full `b"..."` literal text under `mode`.
181///
182/// Forced ASCII/UTF-16LE modes are best-effort: bytes (or code units) that
183/// aren't printable are escaped rather than rejected, so the user always sees
184/// the override they asked for.
185pub fn render_bytes_literal(data: &[u8], mode: BytesDisplay) -> String {
186    let mut out = String::new();
187    out.push_str("b\"");
188    match mode {
189        BytesDisplay::Auto => {
190            if let Some((_, s)) = decode_string(data) {
191                out.push_str(&escape_decoded(&s));
192            } else {
193                for &b in data {
194                    out.push_str(&format!("\\x{b:02x}"));
195                }
196            }
197        }
198        BytesDisplay::Ascii => {
199            for &b in data {
200                push_ascii_byte(&mut out, b);
201            }
202        }
203        BytesDisplay::Utf16Le => {
204            let (pairs, remainder) = data.as_chunks::<2>();
205            for &pair in pairs {
206                let unit = u16::from_le_bytes(pair);
207                match char::from_u32(unit as u32) {
208                    Some(ch) if !ch.is_control() => match ch {
209                        '"' | '\\' => {
210                            out.push('\\');
211                            out.push(ch);
212                        }
213                        _ => out.push(ch),
214                    },
215                    _ => out.push_str(&format!("\\u{{{unit:04x}}}")),
216                }
217            }
218            // Trailing odd byte, if any.
219            for &b in remainder {
220                out.push_str(&format!("\\x{b:02x}"));
221            }
222        }
223        BytesDisplay::Raw => {
224            for &b in data {
225                out.push_str(&format!("\\x{b:02x}"));
226            }
227        }
228    }
229    out.push('"');
230    out
231}
232
233impl std::fmt::Display for BytesRef<'_, '_> {
234    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235        let data = &self.ctx.values.bytes[self.id].data;
236        let mode = self.ctx.bytes_display(self.id);
237        if mode != BytesDisplay::Auto {
238            return f.write_str(&render_bytes_literal(data, mode));
239        }
240        if let Some((_, s)) = decode_string(data) {
241            return write!(f, "b\"{}\"", escape_decoded(&s));
242        }
243        write!(f, "b\"")?;
244        for &b in data {
245            write!(f, "\\x{:02x}", b)?;
246        }
247        write!(f, "\"")
248    }
249}
250
251impl<'str, 'ctx> Value<'str, 'ctx> for BytesRef<'str, 'ctx> {
252    fn id(&self) -> ValueId {
253        ValueId::Bytes(self.id)
254    }
255
256    fn size(&self) -> usize {
257        self.ctx.values.bytes[self.id].data.len()
258    }
259}