Skip to main content

oxinum_int/native/
bytes_signed.rs

1//! Two's-complement signed byte serialization for [`BigInt`].
2//!
3//! This module adds inherent methods on [`BigInt`] for converting to and from
4//! big-endian / little-endian two's-complement byte sequences. The encoding
5//! uses **minimal length**: the byte string is as short as possible while
6//! still unambiguously representing the signed value under two's-complement
7//! sign-extension semantics.
8//!
9//! # Encoding rules (big-endian)
10//!
11//! - **Zero** → `[0x00]` (single zero byte, NOT empty — distinguishes zero
12//!   from a zero-length / sentinel value).
13//! - **Positive** `n`: take the BE bytes of `|n|`; if the top byte's
14//!   most-significant bit is `1`, prepend a `0x00` byte (else the encoding
15//!   would sign-extend to a negative value).
16//! - **Negative** `n`: compute the BE bytes of `|n| - 1`, bitwise-NOT each
17//!   byte, then prepend `0xFF` if the top byte's MSB is `0` (else the
18//!   encoding would sign-extend to a positive value).
19//!
20//! # Decoding rules (big-endian)
21//!
22//! - **Empty input** → `BigInt::zero()`.
23//! - **Non-empty**: inspect the top bit of the first byte. If `0`, the value
24//!   is non-negative; build a [`BigUint`] from the bytes directly. If `1`,
25//!   the value is negative: bitwise-NOT all bytes, add `1`, and use the
26//!   result as the magnitude with `Sign::Negative`.
27//!
28//! Little-endian variants apply the same logic with bytes reversed.
29//!
30//! # Examples
31//!
32//! ```
33//! use oxinum_int::native::BigInt;
34//!
35//! // Round-trip identity.
36//! let n = BigInt::from(-129i64);
37//! let bytes = n.to_signed_bytes_be();
38//! assert_eq!(bytes, vec![0xFFu8, 0x7F]);
39//! assert_eq!(BigInt::from_signed_bytes_be(&bytes), n);
40//!
41//! // -128 fits in one byte (0x80 is exactly -128 in two's complement).
42//! let neg128 = BigInt::from(-128i64);
43//! assert_eq!(neg128.to_signed_bytes_be(), vec![0x80u8]);
44//! ```
45
46use super::int::BigInt;
47use super::uint::BigUint;
48use oxinum_core::Sign;
49
50impl BigInt {
51    /// Returns the two's-complement big-endian byte representation of this
52    /// value, using the minimal number of bytes.
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// use oxinum_int::native::BigInt;
58    ///
59    /// assert_eq!(BigInt::from(0i64).to_signed_bytes_be(), vec![0u8]);
60    /// assert_eq!(BigInt::from(1i64).to_signed_bytes_be(), vec![1u8]);
61    /// assert_eq!(BigInt::from(-1i64).to_signed_bytes_be(), vec![0xFFu8]);
62    /// assert_eq!(BigInt::from(127i64).to_signed_bytes_be(), vec![0x7Fu8]);
63    /// assert_eq!(BigInt::from(-128i64).to_signed_bytes_be(), vec![0x80u8]);
64    /// assert_eq!(BigInt::from(128i64).to_signed_bytes_be(), vec![0x00u8, 0x80]);
65    /// assert_eq!(BigInt::from(129i64).to_signed_bytes_be(), vec![0x00u8, 0x81]);
66    /// assert_eq!(BigInt::from(-129i64).to_signed_bytes_be(), vec![0xFFu8, 0x7F]);
67    /// ```
68    pub fn to_signed_bytes_be(&self) -> Vec<u8> {
69        let mut bytes = self.to_signed_bytes_le();
70        bytes.reverse();
71        bytes
72    }
73
74    /// Returns the two's-complement little-endian byte representation of this
75    /// value, using the minimal number of bytes.
76    ///
77    /// # Examples
78    ///
79    /// ```
80    /// use oxinum_int::native::BigInt;
81    ///
82    /// assert_eq!(BigInt::from(0i64).to_signed_bytes_le(), vec![0u8]);
83    /// assert_eq!(BigInt::from(1i64).to_signed_bytes_le(), vec![1u8]);
84    /// assert_eq!(BigInt::from(-1i64).to_signed_bytes_le(), vec![0xFFu8]);
85    /// assert_eq!(BigInt::from(128i64).to_signed_bytes_le(), vec![0x80u8, 0x00]);
86    /// assert_eq!(BigInt::from(-129i64).to_signed_bytes_le(), vec![0x7Fu8, 0xFF]);
87    /// ```
88    pub fn to_signed_bytes_le(&self) -> Vec<u8> {
89        if self.is_zero() {
90            // Zero is encoded as a single zero byte (NOT empty) to preserve
91            // round-trip with `from_signed_bytes_le`.
92            return vec![0u8];
93        }
94        if self.sign() == Sign::Positive {
95            // Positive: take the unsigned LE bytes, then pad with one 0x00
96            // byte at the high end if the top byte's MSB is 1 (otherwise the
97            // encoding would sign-extend to a negative value).
98            let mut bytes = self.magnitude().to_bytes_le();
99            // `to_bytes_le` strips trailing-zero bytes, which equals the top
100            // bytes in LE order — so the last byte is the most-significant.
101            // Defensive: magnitude is non-zero here, so bytes is non-empty.
102            let last_idx = bytes.len().saturating_sub(1);
103            if bytes[last_idx] & 0x80 != 0 {
104                bytes.push(0x00);
105            }
106            bytes
107        } else {
108            // Negative: bytes of |n| - 1 with each byte bitwise-NOT'd.
109            // |n| - 1 is non-negative because |n| >= 1 (we're in the
110            // negative-strict branch).
111            let mag_minus_one = self
112                .magnitude()
113                .checked_sub(&BigUint::one())
114                .unwrap_or_else(BigUint::zero);
115            let mut bytes = mag_minus_one.to_bytes_le();
116            // `to_bytes_le()` strips trailing zeros in LE-order (i.e. the
117            // high bytes). For two's-complement encoding we need to invert
118            // these implicit-zero high bytes into `0xFF`, but since the
119            // representation is "minimal length", we only emit as many
120            // bytes as needed — high zeros become high 0xFFs by sign
121            // extension at decode time. So we just NOT what is present.
122            for b in bytes.iter_mut() {
123                *b = !*b;
124            }
125            // Now ensure the top byte (last in LE) has its MSB set so the
126            // encoding decodes as negative. If not, push a 0xFF byte.
127            // After NOT'ing, if |n|-1's top byte had MSB=1, NOT'd MSB=0 → push
128            // 0xFF. If |n|-1's top byte had MSB=0 (or bytes is empty because
129            // |n|-1 == 0, i.e. n == -1), we need the final encoding to start
130            // with 0xFF.
131            let need_pad = match bytes.last() {
132                None => true,                    // |n| - 1 == 0 → encode as [0xFF]
133                Some(&top) => (top & 0x80) == 0, // top MSB clear → sign-extend issue
134            };
135            if need_pad {
136                bytes.push(0xFFu8);
137            }
138            bytes
139        }
140    }
141
142    /// Construct a `BigInt` from a two's-complement big-endian byte slice.
143    ///
144    /// An empty slice decodes as zero.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use oxinum_int::native::BigInt;
150    ///
151    /// assert_eq!(BigInt::from_signed_bytes_be(&[]), BigInt::zero());
152    /// assert_eq!(BigInt::from_signed_bytes_be(&[0x00]), BigInt::zero());
153    /// assert_eq!(BigInt::from_signed_bytes_be(&[0x01]), BigInt::from(1i64));
154    /// assert_eq!(BigInt::from_signed_bytes_be(&[0xFF]), BigInt::from(-1i64));
155    /// assert_eq!(BigInt::from_signed_bytes_be(&[0x80]), BigInt::from(-128i64));
156    /// assert_eq!(BigInt::from_signed_bytes_be(&[0xFF, 0x7F]), BigInt::from(-129i64));
157    /// ```
158    pub fn from_signed_bytes_be(bytes: &[u8]) -> BigInt {
159        if bytes.is_empty() {
160            return BigInt::zero();
161        }
162        let top = bytes[0];
163        if top & 0x80 == 0 {
164            // Non-negative: build magnitude directly from the BE bytes.
165            let mag = BigUint::from_bytes_be(bytes);
166            BigInt::from_parts(Sign::Positive, mag)
167        } else {
168            // Negative: bitwise-NOT all bytes and add 1 to recover |n|.
169            let mut inv: Vec<u8> = bytes.iter().map(|b| !*b).collect();
170            // After NOT, build a BigUint and add 1.
171            // Note: NOT on a length-N two's-complement encoding of a negative
172            // value gives |n| - 1 in length-N unsigned form (possibly with
173            // leading zeros).
174            // Reverse to LE for our helper.
175            inv.reverse();
176            let inv_uint = BigUint::from_bytes_le(&inv);
177            let mag = &inv_uint + &BigUint::one();
178            BigInt::from_parts(Sign::Negative, mag)
179        }
180    }
181
182    /// Construct a `BigInt` from a two's-complement little-endian byte slice.
183    ///
184    /// An empty slice decodes as zero.
185    ///
186    /// # Examples
187    ///
188    /// ```
189    /// use oxinum_int::native::BigInt;
190    ///
191    /// assert_eq!(BigInt::from_signed_bytes_le(&[]), BigInt::zero());
192    /// assert_eq!(BigInt::from_signed_bytes_le(&[0xFF]), BigInt::from(-1i64));
193    /// assert_eq!(BigInt::from_signed_bytes_le(&[0x7F, 0xFF]), BigInt::from(-129i64));
194    /// ```
195    pub fn from_signed_bytes_le(bytes: &[u8]) -> BigInt {
196        if bytes.is_empty() {
197            return BigInt::zero();
198        }
199        // Reverse to BE and delegate.
200        let mut be: Vec<u8> = bytes.to_vec();
201        be.reverse();
202        Self::from_signed_bytes_be(&be)
203    }
204}
205
206// ---------------------------------------------------------------------------
207// Tests
208// ---------------------------------------------------------------------------
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn zero_encodes_as_single_zero_byte() {
216        assert_eq!(BigInt::zero().to_signed_bytes_be(), vec![0u8]);
217        assert_eq!(BigInt::zero().to_signed_bytes_le(), vec![0u8]);
218    }
219
220    #[test]
221    fn empty_decodes_as_zero() {
222        assert_eq!(BigInt::from_signed_bytes_be(&[]), BigInt::zero());
223        assert_eq!(BigInt::from_signed_bytes_le(&[]), BigInt::zero());
224    }
225
226    #[test]
227    fn single_zero_byte_decodes_as_zero() {
228        assert_eq!(BigInt::from_signed_bytes_be(&[0x00]), BigInt::zero());
229        assert_eq!(BigInt::from_signed_bytes_le(&[0x00]), BigInt::zero());
230    }
231
232    #[test]
233    fn small_positive_minimal_encoding() {
234        assert_eq!(BigInt::from(1i64).to_signed_bytes_be(), vec![0x01u8]);
235        assert_eq!(BigInt::from(127i64).to_signed_bytes_be(), vec![0x7Fu8]);
236        // 128 needs a leading zero to avoid sign extension.
237        assert_eq!(
238            BigInt::from(128i64).to_signed_bytes_be(),
239            vec![0x00u8, 0x80]
240        );
241        assert_eq!(
242            BigInt::from(129i64).to_signed_bytes_be(),
243            vec![0x00u8, 0x81]
244        );
245    }
246
247    #[test]
248    fn small_negative_minimal_encoding() {
249        assert_eq!(BigInt::from(-1i64).to_signed_bytes_be(), vec![0xFFu8]);
250        // -128 fits in a single byte: 0x80 == -128 in two's complement.
251        assert_eq!(BigInt::from(-128i64).to_signed_bytes_be(), vec![0x80u8]);
252        // -129 needs two bytes (0xFF, 0x7F).
253        assert_eq!(
254            BigInt::from(-129i64).to_signed_bytes_be(),
255            vec![0xFFu8, 0x7F]
256        );
257    }
258}