Skip to main content

obfany/
lib.rs

1/*!
2Compiletime string constant obfuscation.
3*/
4
5#![cfg_attr(not(test), no_std)]
6
7use core::ffi::CStr;
8use core::str;
9
10#[doc(hidden)]
11pub mod wide;
12
13#[doc(hidden)]
14pub mod cfo;
15
16mod murmur3;
17pub use self::murmur3::murmur3;
18
19mod pos;
20pub use self::pos::position;
21
22#[doc(hidden)]
23pub mod xref;
24
25//----------------------------------------------------------------
26
27/// Compiletime random number generator.
28///
29/// Supported types are `u8`, `u16`, `u32`, `u64`, `usize`, `i8`, `i16`, `i32`, `i64`, `isize`, `bool`, `f32` and `f64`.
30///
31/// The integer types generate a random value in their respective range.  
32/// The float types generate a random value in range of `[1.0, 2.0)`.
33///
34/// ```
35/// const RND: i32 = obfany::random!(u8) as i32;
36/// assert!(RND >= 0 && RND <= 255);
37/// # const _: f32 = obfany::random!(f32);
38/// # const _: f64 = obfany::random!(f64);
39/// ```
40///
41/// The behavior of the macro inside other macros can be surprising:
42///
43/// ```
44/// // When used as top-level input to macros, random works as expected
45/// assert_ne!(obfany::random!(u64), obfany::random!(u64));
46///
47/// // When used inside the definition of a macro, random does not work as expected
48/// macro_rules! inside {
49/// 	() => {
50/// 		assert_eq!(obfany::random!(u64), obfany::random!(u64));
51/// 	};
52/// }
53/// inside!();
54///
55/// // When provided a unique seed, random works as expected
56/// // Note that the seeds must evaluate to a literal!
57/// macro_rules! seeded {
58/// 	() => {
59/// 		assert_ne!(obfany::random!(u64, "lhs"), obfany::random!(u64, "rhs"));
60/// 	};
61/// }
62/// seeded!();
63///
64/// // Repeated usage in macros, random does not work as expected
65/// macro_rules! repeated {
66/// 	($($name:ident),*) => {
67/// 		$(let $name = obfany::random!(u64, "seed");)*
68/// 	};
69/// }
70/// repeated!(a, b);
71/// assert_eq!(a, b);
72///
73/// // Provide additional unique seeds, random works as expected
74/// macro_rules! repeated_seeded {
75/// 	($($name:ident),*) => {
76/// 		$(let $name = obfany::random!(u64, "seed", stringify!($name));)*
77/// 	};
78/// }
79/// repeated_seeded!(c, d);
80/// assert_ne!(c, d);
81/// ```
82#[macro_export]
83macro_rules! random {
84	($ty:ident $(, $seeds:expr)* $(,)?) => {{
85		const _RANDOM: $ty = $crate::__random_cast!($ty,
86			$crate::entropy(concat!(file!(), ":", line!(), ":", column!() $(, ":", $seeds)*)));
87		_RANDOM
88	}};
89}
90
91#[doc(hidden)]
92#[macro_export]
93macro_rules! __random_cast {
94    (u8, $seed:expr) => {
95        $seed as u8
96    };
97    (u16, $seed:expr) => {
98        $seed as u16
99    };
100    (u32, $seed:expr) => {
101        $seed as u32
102    };
103    (u64, $seed:expr) => {
104        $seed
105    };
106    (usize, $seed:expr) => {
107        $seed as usize
108    };
109    (i8, $seed:expr) => {
110        $seed as i8
111    };
112    (i16, $seed:expr) => {
113        $seed as i16
114    };
115    (i32, $seed:expr) => {
116        $seed as i32
117    };
118    (i64, $seed:expr) => {
119        $seed as i64
120    };
121    (isize, $seed:expr) => {
122        $seed as isize
123    };
124    (bool, $seed:expr) => {
125        $seed as i64 >= 0
126    };
127
128    // f32::from_bits and f64::from_bits are stable as const fn since Rust 1.46
129    (f32, $seed:expr) => {
130        f32::from_bits(0b0_01111111 << (f32::MANTISSA_DIGITS - 1) | ($seed as u32 >> 9))
131    };
132    (f64, $seed:expr) => {
133        f64::from_bits(0b0_01111111111 << (f64::MANTISSA_DIGITS - 1) | ($seed >> 12))
134    };
135
136    ($ty:ident, $seed:expr) => {
137        compile_error!(concat!("unsupported type: ", stringify!($ty)))
138    };
139}
140
141#[test]
142fn test_random_f32() {
143    fn t(v: f32) {
144        assert!(v >= 1.0 && v < 2.0, "{}", v);
145    }
146    // Multiple calls with different seeds ensure coverage across the float range
147    t(random!(f32, "a"));
148    t(random!(f32, "b"));
149    t(random!(f32, "c"));
150    // Unseeded call uses file!(), line!(), column!()
151    t(random!(f32));
152}
153
154#[test]
155fn test_random_f64() {
156    fn t(v: f64) {
157        assert!(v >= 1.0 && v < 2.0, "{}", v);
158    }
159    t(random!(f64, "a"));
160    t(random!(f64, "b"));
161    t(random!(f64, "c"));
162    t(random!(f64));
163}
164
165#[test]
166fn test_random_int_types() {
167    // Test that all supported integer types compile and produce in-range values
168    let _: u8 = random!(u8, "u8");
169    let _: u16 = random!(u16, "u16");
170    let _: u32 = random!(u32, "u32");
171    let _: u64 = random!(u64, "u64");
172    let _: usize = random!(usize, "usize");
173    let _: i8 = random!(i8, "i8");
174    let _: i16 = random!(i16, "i16");
175    let _: i32 = random!(i32, "i32");
176    let _: i64 = random!(i64, "i64");
177    let _: isize = random!(isize, "isize");
178    let _: bool = random!(bool, "bool");
179    // Different seeds produce different values (random! uses column!() so same-seed is impossible)
180    assert_ne!(random!(u64, "x"), random!(u64, "y"));
181}
182
183/// Compiletime bitmixing.
184///
185/// Takes an intermediate hash that may not be thoroughly mixed and increase its entropy to obtain both better distribution.
186/// See [Better Bit Mixing](https://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html) for reference.
187#[inline(always)]
188pub const fn splitmix(seed: u64) -> u64 {
189    let next = seed.wrapping_add(0x9e3779b97f4a7c15);
190    let mut z = next;
191    z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
192    z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
193    return z ^ (z >> 31);
194}
195
196#[test]
197fn test_splitmix() {
198    // Non-zero output for zero input (avalanche)
199    assert_ne!(splitmix(0), 0);
200    // Deterministic: same input always produces same output
201    assert_eq!(splitmix(42), splitmix(42));
202    // Different seeds produce different outputs
203    assert_ne!(splitmix(1), splitmix(2));
204}
205
206/// Compiletime string constant hash.
207///
208/// Implemented using the [DJB2 hash function](http://www.cse.yorku.ca/~oz/hash.html#djb2) xor variation.
209#[inline(always)]
210pub const fn hash(s: &str) -> u32 {
211    let s = s.as_bytes();
212    let mut result = 3581u32;
213    let mut i = 0usize;
214    while i < s.len() {
215        result = result.wrapping_mul(33) ^ s[i] as u32;
216        i += 1;
217    }
218    return result;
219}
220
221#[test]
222fn test_hash() {
223    // Known answer test (matches doctest)
224    assert_eq!(hash!("Hello World"), 0x6E4A573D);
225    // Empty string
226    let _ = hash!("");
227    // Unicode
228    let _ = hash!("🦀");
229    // Determinism
230    assert_eq!(hash!("abc"), hash!("abc"));
231    // Different inputs produce different hashes (probabilistic)
232    assert_ne!(hash!("abc"), hash!("abd"));
233}
234
235/// Compiletime string constant hash.
236///
237/// Helper macro guarantees compiletime evaluation of the string constant hash.
238///
239/// ```
240/// const STRING: &str = "Hello World";
241/// assert_eq!(obfany::hash!(STRING), 0x6E4A573D);
242/// ```
243#[macro_export]
244macro_rules! hash {
245    ($s:expr) => {{
246        const _DJB2_HASH: u32 = $crate::hash($s);
247        _DJB2_HASH
248    }};
249}
250
251/// Produces pseudorandom entropy from the given string.
252#[doc(hidden)]
253#[inline(always)]
254pub const fn entropy(string: &str) -> u64 {
255    splitmix(SEED ^ splitmix(hash(string) as u64))
256}
257
258/// Compiletime RNG seed.
259///
260/// This value is derived from the environment variable `OBFANY_SEED` and has a fixed value if absent.
261/// If it changes all downstream dependents are recompiled automatically.
262pub const SEED: u64 = splitmix(hash(match option_env!("OBFANY_SEED") {
263    Some(seed) => seed,
264    None => "FIXED",
265}) as u64);
266
267//----------------------------------------------------------------
268
269#[doc(hidden)]
270pub mod bytes;
271
272#[doc(hidden)]
273pub mod words;
274
275#[doc(hidden)]
276#[inline(always)]
277pub const fn unsafe_as_str(bytes: &[u8]) -> &str {
278    // When used correctly by this crate's macros this should be safe
279    #[cfg(debug_assertions)]
280    return match str::from_utf8(bytes) {
281        Ok(s) => s,
282        Err(_) => panic!("invalid str"),
283    };
284    #[cfg(not(debug_assertions))]
285    return unsafe { str::from_utf8_unchecked(bytes) };
286}
287
288#[doc(hidden)]
289#[inline(always)]
290pub const fn unsafe_as_cstr(bytes: &[u8]) -> &CStr {
291    // When used correctly by this crate's macros this should be safe
292    #[cfg(debug_assertions)]
293    return match CStr::from_bytes_with_nul(bytes) {
294        Ok(cstr) => cstr,
295        Err(_) => panic!("invalid cstr"),
296    };
297    #[cfg(not(debug_assertions))]
298    return unsafe { CStr::from_bytes_with_nul_unchecked(bytes) };
299}