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}