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}