stygian_browser/
rects_noise.rs1use crate::noise::NoiseEngine;
20
21#[must_use]
41pub fn rects_noise_script(engine: &NoiseEngine) -> String {
42 let noise_fn = engine.js_noise_fn();
43 format!(
44 r"(function() {{
45 'use strict';
46
47 // ── Noise helpers ──────────────────────────────────────────────────────
48 {noise_fn}
49
50 // ── Spoof toString ─────────────────────────────────────────────────────
51 function _nts(name) {{ return function toString() {{ return 'function ' + name + '() {{ [native code] }}'; }}; }}
52 function _def(obj, prop, fn) {{
53 fn.toString = _nts(prop);
54 Object.defineProperty(obj, prop, {{ value: fn, writable: false, configurable: false, enumerable: false }});
55 }}
56
57 // ── Element hash (stable, no DOM dependencies) ─────────────────────────
58 // Combines tagName, className, and textContent length into a u32 index
59 // for use as the noise key coordinate.
60 function _elemHash(el) {{
61 let h = 0;
62 const tag = (el.tagName || '');
63 const cls = (el.className && typeof el.className === 'string' ? el.className : '');
64 const tlen = (el.textContent ? el.textContent.length : 0);
65 for (let i = 0; i < tag.length; i++) h = ((h * 31) + tag.charCodeAt(i)) & 0xFFFFFFFF;
66 for (let i = 0; i < cls.length; i++) h = ((h * 31) + cls.charCodeAt(i)) & 0xFFFFFFFF;
67 h = ((h * 31) + tlen) & 0xFFFFFFFF;
68 return h >>> 0;
69 }}
70
71 // ── Noise a DOMRect preserving consistency ──────────────────────────────
72 function _noiseRect(rect, key, idx) {{
73 const [dx, dy, dw, dh] = __stygian_rect_noise(key, idx);
74 const x = rect.x + dx;
75 const y = rect.y + dy;
76 const w = rect.width + dw;
77 const h = rect.height + dh;
78 return {{
79 x: x, y: y, width: w, height: h,
80 left: x, top: y, right: x + w, bottom: y + h,
81 toJSON: function() {{
82 return {{ x: x, y: y, width: w, height: h, left: x, top: y, right: x + w, bottom: y + h }};
83 }}
84 }};
85 }}
86
87 // ── Element.getBoundingClientRect ───────────────────────────────────────
88 if (typeof Element !== 'undefined') {{
89 const _origGBCR = Element.prototype.getBoundingClientRect;
90 _def(Element.prototype, 'getBoundingClientRect', function getBoundingClientRect() {{
91 const r = _origGBCR.call(this);
92 return _noiseRect(r, 'rects.getBCR', _elemHash(this));
93 }});
94
95 // Element.getClientRects
96 const _origGCR = Element.prototype.getClientRects;
97 _def(Element.prototype, 'getClientRects', function getClientRects() {{
98 const list = _origGCR.call(this);
99 const h = _elemHash(this);
100 const result = [];
101 for (let i = 0; i < list.length; i++) {{
102 result.push(_noiseRect(list[i], 'rects.getCR', (h + i) >>> 0));
103 }}
104 result[Symbol.iterator] = Array.prototype[Symbol.iterator];
105 result.item = function(idx) {{ return result[idx] || null; }};
106 result.length = result.length;
107 return result;
108 }});
109 }}
110
111 // ── Range.getBoundingClientRect ─────────────────────────────────────────
112 if (typeof Range !== 'undefined') {{
113 const _origRGBCR = Range.prototype.getBoundingClientRect;
114 _def(Range.prototype, 'getBoundingClientRect', function getBoundingClientRect() {{
115 const r = _origRGBCR.call(this);
116 return _noiseRect(r, 'rects.rangeBCR', 0);
117 }});
118
119 const _origRGCR = Range.prototype.getClientRects;
120 _def(Range.prototype, 'getClientRects', function getClientRects() {{
121 const list = _origRGCR.call(this);
122 const result = [];
123 for (let i = 0; i < list.length; i++) {{
124 result.push(_noiseRect(list[i], 'rects.rangeCR', i));
125 }}
126 result[Symbol.iterator] = Array.prototype[Symbol.iterator];
127 result.item = function(idx) {{ return result[idx] || null; }};
128 result.length = result.length;
129 return result;
130 }});
131 }}
132
133 // ── CanvasRenderingContext2D.measureText ────────────────────────────────
134 if (typeof CanvasRenderingContext2D !== 'undefined') {{
135 const _origMT = CanvasRenderingContext2D.prototype.measureText;
136 _def(CanvasRenderingContext2D.prototype, 'measureText', function measureText(text) {{
137 const m = _origMT.call(this, text);
138 // Hash the text for a stable noise index
139 let th = 0;
140 for (let i = 0; i < text.length; i++) th = ((th * 31) + text.charCodeAt(i)) & 0xFFFFFFFF;
141 const [dx, , dw, ] = __stygian_rect_noise('rects.measureText', th >>> 0);
142 const scale = 0.01; // ±0.001..0.01 pixels
143 return {{
144 width: m.width + dx * scale,
145 actualBoundingBoxLeft: m.actualBoundingBoxLeft + dx * scale,
146 actualBoundingBoxRight: m.actualBoundingBoxRight + dw * scale,
147 actualBoundingBoxAscent: m.actualBoundingBoxAscent + dx * scale,
148 actualBoundingBoxDescent: m.actualBoundingBoxDescent + dw * scale,
149 fontBoundingBoxAscent: m.fontBoundingBoxAscent + dx * scale,
150 fontBoundingBoxDescent: m.fontBoundingBoxDescent + dw * scale,
151 emHeightAscent: m.emHeightAscent + dx * scale,
152 emHeightDescent: m.emHeightDescent + dw * scale,
153 hangingBaseline: m.hangingBaseline + dx * scale,
154 alphabeticBaseline: m.alphabeticBaseline + dx * scale,
155 ideographicBaseline: m.ideographicBaseline + dx * scale,
156 }};
157 }});
158 }}
159
160}})();
161"
162 )
163}
164
165#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::noise::{NoiseEngine, NoiseSeed};
173
174 fn eng(seed: u64) -> NoiseEngine {
175 NoiseEngine::new(NoiseSeed::from(seed))
176 }
177
178 #[test]
179 fn script_overrides_all_methods() {
180 let js = rects_noise_script(&eng(1));
181 assert!(
182 js.contains("getBoundingClientRect"),
183 "missing getBoundingClientRect"
184 );
185 assert!(js.contains("getClientRects"), "missing getClientRects");
186 assert!(js.contains("Range"), "missing Range overrides");
187 assert!(js.contains("measureText"), "missing measureText");
188 }
189
190 #[test]
191 fn script_preserves_domrect_consistency() {
192 let js = rects_noise_script(&eng(1));
193 assert!(
195 js.contains("right: x + w"),
196 "DOMRect right not derived from x+w"
197 );
198 assert!(
199 js.contains("bottom: y + h"),
200 "DOMRect bottom not derived from y+h"
201 );
202 }
203
204 #[test]
205 fn text_metrics_covers_8_properties() {
206 let js = rects_noise_script(&eng(1));
207 let required = [
208 "width",
209 "actualBoundingBoxLeft",
210 "actualBoundingBoxRight",
211 "actualBoundingBoxAscent",
212 "actualBoundingBoxDescent",
213 "fontBoundingBoxAscent",
214 "fontBoundingBoxDescent",
215 "emHeightAscent",
216 ];
217 for prop in &required {
218 assert!(js.contains(prop), "TextMetrics missing {prop}");
219 }
220 }
221
222 #[test]
223 fn script_contains_rect_noise_fn() {
224 let js = rects_noise_script(&eng(1));
225 assert!(js.contains("__stygian_rect_noise"), "missing rect noise fn");
226 }
227
228 #[test]
229 fn element_hash_handles_null_classname() {
230 let js = rects_noise_script(&eng(1));
231 assert!(
233 js.contains("typeof el.className === 'string'"),
234 "className guard missing"
235 );
236 }
237
238 #[test]
239 fn script_contains_native_tostring() {
240 let js = rects_noise_script(&eng(1));
241 assert!(js.contains("[native code]"), "missing toString spoof");
242 }
243}