Skip to main content

otter/types/terms/
binary.rs

1//! [`Binary`] (byte-aligned) and [`Bitstring`] (possibly sub-byte) — Erlang
2//! binary data, viewed zero-copy into the BEAM heap. Also the binary side of
3//! external-term-format (de)serialization.
4
5use core::marker::PhantomData;
6use std::str::Utf8Error;
7
8use crate::types::sealed::Sealed;
9use crate::types::{AnyTerm, Env, Invariant, RawTerm, Term};
10
11/// A byte-aligned binary (`enif_is_binary` returned true).
12///
13/// Data is on the BEAM heap. Nothing is copied until `as_bytes`/`from_bytes` is
14/// called. The byte accessors take an `impl Env<'id>` (this brand) because the
15/// term carries only its brand, not its env.
16#[derive(Clone, Copy)]
17pub struct Binary<'id> {
18    raw_term: RawTerm,
19    _id: Invariant<'id>,
20}
21
22/// An Erlang bitstring (`enif_term_type` returned `Bitstring`).
23///
24/// In BEAM every binary is a bitstring; a `Bitstring` may be byte-aligned or
25/// sub-byte. Call [`is_binary`](Self::is_binary) / [`to_binary`](Self::to_binary)
26/// to refine. The NIF API offers no inspection of the sub-byte case.
27#[derive(Clone, Copy)]
28pub struct Bitstring<'id> {
29    raw_term: RawTerm,
30    _id: Invariant<'id>,
31}
32
33impl<'id> Bitstring<'id> {
34    #[crate::raw]
35    pub(crate) fn from_raw(raw_term: RawTerm) -> Self {
36        Self { raw_term, _id: PhantomData }
37    }
38
39    /// Returns `true` if this bitstring is byte-aligned (`enif_is_binary`).
40    pub fn is_binary(self, env: impl Env<'id>) -> bool {
41        unsafe { enif_ffi::is_binary(env.raw_env(), self.raw_term) != 0 }
42    }
43
44    /// View as a [`Binary`] if byte-aligned, else `None`.
45    pub fn to_binary(self, env: impl Env<'id>) -> Option<Binary<'id>> {
46        self.is_binary(env)
47            .then_some(Binary { raw_term: self.raw_term, _id: PhantomData })
48    }
49}
50
51impl<'id> Binary<'id> {
52    #[crate::raw]
53    pub(crate) fn from_raw(raw_term: RawTerm) -> Self {
54        Self { raw_term, _id: PhantomData }
55    }
56
57    /// View the binary data as a byte slice (`enif_inspect_binary`).
58    ///
59    /// Zero-copy — the slice points into the BEAM heap and rides this binary's
60    /// brand `'id`, which cannot escape its env's scope.
61    pub fn as_bytes(self, env: impl Env<'id>) -> &'id [u8] {
62        let mut bin: enif_ffi::Binary = unsafe { std::mem::zeroed() };
63        let ok = unsafe { enif_ffi::inspect_binary(env.raw_env(), self.raw_term, &mut bin) };
64        assert!(ok != 0, "inspect_binary failed on a validated Binary");
65        unsafe { std::slice::from_raw_parts(bin.data, bin.size) }
66    }
67
68    /// Number of bytes in the binary.
69    pub fn len(self, env: impl Env<'id>) -> usize {
70        self.as_bytes(env).len()
71    }
72
73    /// Returns `true` if the binary contains no bytes.
74    pub fn is_empty(self, env: impl Env<'id>) -> bool {
75        self.len(env) == 0
76    }
77
78    /// Interpret the binary as a UTF-8 string (zero-copy). `Err` if the bytes
79    /// are not valid UTF-8.
80    pub fn try_str(self, env: impl Env<'id>) -> Result<&'id str, Utf8Error> {
81        std::str::from_utf8(self.as_bytes(env))
82    }
83
84    /// Create a zero-copy sub-binary term spanning `pos..pos+len`
85    /// (`enif_make_sub_binary`). Panics if out of bounds.
86    pub fn sub(self, env: impl Env<'id>, pos: usize, len: usize) -> Binary<'id> {
87        let total = self.len(env);
88        assert!(
89            pos.checked_add(len).is_some_and(|end| end <= total),
90            "sub-binary out of bounds: pos({pos}) + len({len}) > {total}"
91        );
92        let raw_term = unsafe { enif_ffi::make_sub_binary(env.raw_env(), self.raw_term, pos, len) };
93        Binary { raw_term, _id: PhantomData }
94    }
95
96    /// Allocate a new binary on the BEAM heap and copy `data` into it
97    /// (`enif_make_new_binary`).
98    pub fn from_bytes(env: impl Env<'id>, data: &[u8]) -> Binary<'id> {
99        let mut raw_term: RawTerm = 0;
100        unsafe {
101            let ptr = enif_ffi::make_new_binary(env.raw_env(), data.len(), &mut raw_term);
102            // enif_make_new_binary cannot return null: tracing the ERTS source
103            // (verified across OTP 26 and 27) it allocates via HAlloc or
104            // erts_bin_nrml_alloc, both of which abort the VM on OOM rather than
105            // returning null. The assert pins that invariant so the copy below
106            // never runs against a null destination.
107            assert!(!ptr.is_null(), "enif_make_new_binary returned null");
108            std::ptr::copy_nonoverlapping(data.as_ptr(), ptr, data.len());
109        }
110        Binary { raw_term, _id: PhantomData }
111    }
112
113    /// Returns `true` if `term` is a byte-aligned binary (`enif_is_binary`).
114    /// Sub-byte bitstrings return `false`.
115    pub fn is_binary(env: impl Env<'id>, term: impl Term<'id>) -> bool {
116        unsafe { enif_ffi::is_binary(env.raw_env(), term.raw_term()) != 0 }
117    }
118
119    /// Deserialize a term from this binary's external-term-format contents
120    /// (`enif_binary_to_term`). If `safe`, encoded atoms not already in the atom
121    /// table are rejected. `None` on decode failure.
122    pub fn deserialize(self, env: impl Env<'id>, safe: bool) -> Option<AnyTerm<'id>> {
123        let bytes = self.as_bytes(env);
124        let opts = if safe { enif_ffi::BIN2TERM_SAFE } else { 0 };
125        let mut term: RawTerm = 0;
126        let consumed = unsafe {
127            enif_ffi::binary_to_term(env.raw_env(), bytes.as_ptr(), bytes.len(), &mut term, opts)
128        };
129        (consumed != 0).then(|| AnyTerm::wrap(term, env))
130    }
131}
132
133impl std::fmt::Debug for Binary<'_> {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        write!(f, "Binary")
136    }
137}
138
139impl PartialEq for Binary<'_> {
140    fn eq(&self, other: &Self) -> bool {
141        unsafe { enif_ffi::is_identical(self.raw_term, other.raw_term) != 0 }
142    }
143}
144impl Eq for Binary<'_> {}
145impl PartialOrd for Binary<'_> {
146    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
147        Some(self.cmp(other))
148    }
149}
150impl Ord for Binary<'_> {
151    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
152        unsafe { enif_ffi::compare(self.raw_term, other.raw_term) }.cmp(&0)
153    }
154}
155
156impl std::fmt::Debug for Bitstring<'_> {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        write!(f, "Bitstring")
159    }
160}
161impl PartialEq for Bitstring<'_> {
162    fn eq(&self, other: &Self) -> bool {
163        unsafe { enif_ffi::is_identical(self.raw_term, other.raw_term) != 0 }
164    }
165}
166impl Eq for Bitstring<'_> {}
167impl PartialOrd for Bitstring<'_> {
168    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
169        Some(self.cmp(other))
170    }
171}
172impl Ord for Bitstring<'_> {
173    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
174        unsafe { enif_ffi::compare(self.raw_term, other.raw_term) }.cmp(&0)
175    }
176}
177
178impl<'id> Sealed for Binary<'id> {}
179impl<'id> Term<'id> for Binary<'id> {
180    fn raw_term(self) -> RawTerm {
181        self.raw_term
182    }
183}
184
185impl<'id> Sealed for Bitstring<'id> {}
186impl<'id> Term<'id> for Bitstring<'id> {
187    fn raw_term(self) -> RawTerm {
188        self.raw_term
189    }
190}