Skip to main content

fxcp_core/nfs/
xdr.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2// Copyright (C) 2025 Joel Wirāmu Pauling <aenertia@aenertia.net>
3//
4// fxcp-core/src/nfs/xdr.rs  --  Minimal XDR encoder/decoder for NFSv4.2
5
6//! Zero-dependency XDR (External Data Representation) encoder and decoder.
7//!
8//! Implements the subset of XDR needed for NFSv4.2 compound RPCs:
9//! big-endian integers, length-prefixed opaque data with 4-byte padding,
10//! and UTF-8 strings.
11
12/// XDR encoder that writes into a pre-allocated `Vec<u8>`.
13pub struct XdrEncoder {
14    buf: Vec<u8>,
15}
16
17impl XdrEncoder {
18    /// Create a new encoder with the given initial capacity.
19    pub fn new(capacity: usize) -> Self {
20        Self { buf: Vec::with_capacity(capacity) }
21    }
22
23    /// Encode a 32-bit unsigned integer (big-endian).
24    pub fn encode_u32(&mut self, v: u32) {
25        self.buf.extend_from_slice(&v.to_be_bytes());
26    }
27
28    /// Encode a 32-bit signed integer (big-endian).
29    pub fn encode_i32(&mut self, v: i32) {
30        self.buf.extend_from_slice(&v.to_be_bytes());
31    }
32
33    /// Encode a 64-bit unsigned integer (big-endian, as two u32s).
34    pub fn encode_u64(&mut self, v: u64) {
35        self.buf.extend_from_slice(&v.to_be_bytes());
36    }
37
38    /// Encode a 64-bit signed integer (big-endian).
39    pub fn encode_i64(&mut self, v: i64) {
40        self.buf.extend_from_slice(&v.to_be_bytes());
41    }
42
43    /// Encode a boolean as a u32 (0 or 1).
44    pub fn encode_bool(&mut self, v: bool) {
45        self.encode_u32(if v { 1 } else { 0 });
46    }
47
48    /// Encode variable-length opaque data: u32 length + data + padding to 4-byte boundary.
49    pub fn encode_opaque(&mut self, data: &[u8]) {
50        self.encode_u32(data.len() as u32);
51        self.buf.extend_from_slice(data);
52        // Pad to 4-byte boundary
53        let pad = (4 - (data.len() % 4)) % 4;
54        for _ in 0..pad {
55            self.buf.push(0);
56        }
57    }
58
59    /// Encode fixed-length opaque data (no length prefix, just data + padding).
60    pub fn encode_opaque_fixed(&mut self, data: &[u8]) {
61        self.buf.extend_from_slice(data);
62        let pad = (4 - (data.len() % 4)) % 4;
63        for _ in 0..pad {
64            self.buf.push(0);
65        }
66    }
67
68    /// Encode a UTF-8 string (same wire format as opaque).
69    pub fn encode_string(&mut self, s: &str) {
70        self.encode_opaque(s.as_bytes());
71    }
72
73    /// Append raw bytes without XDR framing (no length prefix, no padding).
74    pub fn encode_raw(&mut self, data: &[u8]) {
75        self.buf.extend_from_slice(data);
76    }
77
78    /// Consume the encoder and return the encoded bytes.
79    pub fn into_bytes(self) -> Vec<u8> {
80        self.buf
81    }
82
83    /// Current encoded length in bytes.
84    pub fn len(&self) -> usize {
85        self.buf.len()
86    }
87
88    /// Whether the buffer is empty.
89    pub fn is_empty(&self) -> bool {
90        self.buf.is_empty()
91    }
92
93    /// Get a reference to the internal buffer.
94    pub fn as_bytes(&self) -> &[u8] {
95        &self.buf
96    }
97}
98
99/// XDR decoder for parsing NFS4 compound replies.
100pub struct XdrDecoder<'a> {
101    data: &'a [u8],
102    pos: usize,
103}
104
105impl<'a> XdrDecoder<'a> {
106    /// Create a new decoder over the given data.
107    pub fn new(data: &'a [u8]) -> Self {
108        Self { data, pos: 0 }
109    }
110
111    /// Decode a 32-bit unsigned integer.
112    pub fn decode_u32(&mut self) -> Result<u32, XdrError> {
113        if self.pos + 4 > self.data.len() {
114            return Err(XdrError::Truncated);
115        }
116        let v = u32::from_be_bytes([
117            self.data[self.pos], self.data[self.pos + 1],
118            self.data[self.pos + 2], self.data[self.pos + 3],
119        ]);
120        self.pos += 4;
121        Ok(v)
122    }
123
124    /// Decode a 32-bit signed integer.
125    pub fn decode_i32(&mut self) -> Result<i32, XdrError> {
126        Ok(self.decode_u32()? as i32)
127    }
128
129    /// Decode a 64-bit unsigned integer.
130    pub fn decode_u64(&mut self) -> Result<u64, XdrError> {
131        if self.pos + 8 > self.data.len() {
132            return Err(XdrError::Truncated);
133        }
134        let v = u64::from_be_bytes([
135            self.data[self.pos], self.data[self.pos + 1],
136            self.data[self.pos + 2], self.data[self.pos + 3],
137            self.data[self.pos + 4], self.data[self.pos + 5],
138            self.data[self.pos + 6], self.data[self.pos + 7],
139        ]);
140        self.pos += 8;
141        Ok(v)
142    }
143
144    /// Decode variable-length opaque data (reads length, then data + padding).
145    pub fn decode_opaque(&mut self) -> Result<&'a [u8], XdrError> {
146        let len = self.decode_u32()? as usize;
147        if self.pos + len > self.data.len() {
148            return Err(XdrError::Truncated);
149        }
150        let data = &self.data[self.pos..self.pos + len];
151        let padded = len + ((4 - (len % 4)) % 4);
152        self.pos += padded;
153        Ok(data)
154    }
155
156    /// Decode a fixed-length opaque block (no length prefix).
157    pub fn decode_opaque_fixed(&mut self, len: usize) -> Result<&'a [u8], XdrError> {
158        if self.pos + len > self.data.len() {
159            return Err(XdrError::Truncated);
160        }
161        let data = &self.data[self.pos..self.pos + len];
162        let padded = len + ((4 - (len % 4)) % 4);
163        self.pos += padded;
164        Ok(data)
165    }
166
167    /// Skip `n` bytes (with 4-byte alignment).
168    pub fn skip(&mut self, n: usize) -> Result<(), XdrError> {
169        let padded = n + ((4 - (n % 4)) % 4);
170        if self.pos + padded > self.data.len() {
171            return Err(XdrError::Truncated);
172        }
173        self.pos += padded;
174        Ok(())
175    }
176
177    /// Skip raw bytes without alignment.
178    pub fn skip_raw(&mut self, n: usize) -> Result<(), XdrError> {
179        if self.pos + n > self.data.len() {
180            return Err(XdrError::Truncated);
181        }
182        self.pos += n;
183        Ok(())
184    }
185
186    /// Remaining bytes in the buffer.
187    pub fn remaining(&self) -> usize {
188        self.data.len().saturating_sub(self.pos)
189    }
190
191    /// Current position in the buffer.
192    pub fn position(&self) -> usize {
193        self.pos
194    }
195}
196
197/// XDR decoding errors.
198#[derive(Debug, Clone)]
199pub enum XdrError {
200    /// Not enough data remaining to decode the value.
201    Truncated,
202    /// Opaque length exceeds reasonable bounds.
203    InvalidLength,
204}
205
206impl std::fmt::Display for XdrError {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        match self {
209            Self::Truncated => write!(f, "XDR: unexpected end of data"),
210            Self::InvalidLength => write!(f, "XDR: invalid opaque length"),
211        }
212    }
213}
214
215impl std::error::Error for XdrError {}
216
217#[cfg(test)]
218mod tests {
219    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
220    use super::*;
221
222    #[test]
223    fn test_u32_roundtrip() {
224        let mut enc = XdrEncoder::new(64);
225        enc.encode_u32(0);
226        enc.encode_u32(1);
227        enc.encode_u32(0xDEADBEEF);
228        enc.encode_u32(u32::MAX);
229
230        let bytes = enc.into_bytes();
231        let mut dec = XdrDecoder::new(&bytes);
232        assert_eq!(dec.decode_u32().unwrap(), 0);
233        assert_eq!(dec.decode_u32().unwrap(), 1);
234        assert_eq!(dec.decode_u32().unwrap(), 0xDEADBEEF);
235        assert_eq!(dec.decode_u32().unwrap(), u32::MAX);
236        assert_eq!(dec.remaining(), 0);
237    }
238
239    #[test]
240    fn test_u64_roundtrip() {
241        let mut enc = XdrEncoder::new(64);
242        enc.encode_u64(0x0102030405060708);
243        let bytes = enc.into_bytes();
244        let mut dec = XdrDecoder::new(&bytes);
245        assert_eq!(dec.decode_u64().unwrap(), 0x0102030405060708);
246    }
247
248    #[test]
249    fn test_opaque_roundtrip() {
250        let mut enc = XdrEncoder::new(64);
251        enc.encode_opaque(b"hello");     // 5 bytes + 3 padding
252        enc.encode_opaque(b"test");      // 4 bytes + 0 padding
253        enc.encode_opaque(b"x");         // 1 byte + 3 padding
254        enc.encode_opaque(b"");          // 0 bytes
255
256        let bytes = enc.into_bytes();
257        assert_eq!(bytes.len(), 4+8 + 4+4 + 4+4 + 4); // length+padded for each
258
259        let mut dec = XdrDecoder::new(&bytes);
260        assert_eq!(dec.decode_opaque().unwrap(), b"hello");
261        assert_eq!(dec.decode_opaque().unwrap(), b"test");
262        assert_eq!(dec.decode_opaque().unwrap(), b"x");
263        assert_eq!(dec.decode_opaque().unwrap(), b"");
264        assert_eq!(dec.remaining(), 0);
265    }
266
267    #[test]
268    fn test_string_roundtrip() {
269        let mut enc = XdrEncoder::new(64);
270        enc.encode_string("foxing");
271        let bytes = enc.into_bytes();
272        let mut dec = XdrDecoder::new(&bytes);
273        let s = dec.decode_opaque().unwrap();
274        assert_eq!(std::str::from_utf8(s).unwrap(), "foxing");
275    }
276
277    #[test]
278    fn test_bool_encode() {
279        let mut enc = XdrEncoder::new(16);
280        enc.encode_bool(true);
281        enc.encode_bool(false);
282        let bytes = enc.into_bytes();
283        assert_eq!(&bytes, &[0, 0, 0, 1, 0, 0, 0, 0]);
284    }
285
286    #[test]
287    fn test_decoder_truncated() {
288        let data = [0u8; 3]; // Not enough for a u32
289        let mut dec = XdrDecoder::new(&data);
290        assert!(matches!(dec.decode_u32(), Err(XdrError::Truncated)));
291    }
292
293    #[test]
294    fn test_padding_alignment() {
295        // Opaque of length 1 should produce 4+1+3 = 8 bytes on wire
296        let mut enc = XdrEncoder::new(16);
297        enc.encode_opaque(&[0xFF]);
298        let bytes = enc.into_bytes();
299        assert_eq!(bytes.len(), 8);
300        assert_eq!(&bytes, &[0, 0, 0, 1, 0xFF, 0, 0, 0]);
301    }
302}