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}