use bytes::{Buf, Bytes};
use super::acl::{decode_acl, skip_acl};
use super::attrnum;
use crate::Time;
use crate::error::{NfsError, Result};
use crate::mount::Attr;
pub(crate) fn standard_getattr_bitmap() -> [u32; 2] {
let word0: u32 = (1 << 1) | (1 << 4) | (1 << 8) | (1 << attrnum::ACL) | (1 << attrnum::FILEHANDLE) | (1 << attrnum::FILEID);
let word1: u32 = (1 << 1) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 9) | (1 << 13) | (1 << 15) | (1 << 20) | (1 << 21);
[word0, word1]
}
pub(crate) fn encode_setattr(
mode: Option<u32>,
uid: Option<u32>,
gid: Option<u32>,
size: Option<u64>,
atime: Option<Time>,
mtime: Option<Time>,
) -> (Vec<u32>, Vec<u8>) {
let mut word0 = 0u32;
let mut word1 = 0u32;
let mut values = Vec::new();
if let Some(value) = size {
word0 |= 1 << 4;
values.extend_from_slice(&value.to_be_bytes());
}
if let Some(value) = mode {
word1 |= 1 << 1;
values.extend_from_slice(&value.to_be_bytes());
}
for (value, bit) in [(uid, 4u32), (gid, 5u32)] {
if let Some(value) = value {
word1 |= 1 << bit;
let value = value.to_string();
let bytes = value.as_bytes();
values.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
values.extend_from_slice(bytes);
values.resize(values.len() + (4 - bytes.len() % 4) % 4, 0);
}
}
for (value, bit) in [(atime, 16u32), (mtime, 22u32)] {
if let Some(value) = value {
word1 |= 1 << bit;
values.extend_from_slice(&1u32.to_be_bytes());
values.extend_from_slice(&(value.seconds as i64).to_be_bytes());
values.extend_from_slice(&value.nseconds.to_be_bytes());
}
}
let mask = if word1 != 0 {
vec![word0, word1]
} else if word0 != 0 {
vec![word0]
} else {
Vec::new()
};
(mask, values)
}
fn bitmap_has(bitmap: &[u32], attr_num: u32) -> bool {
let word_idx = (attr_num / 32) as usize;
let bit_idx = attr_num % 32;
if word_idx >= bitmap.len() {
return false;
}
(bitmap[word_idx] & (1 << bit_idx)) != 0
}
pub(crate) fn decode_fattr4_envelope(data: &mut Bytes, label: &str) -> Result<(Vec<u32>, Bytes)> {
if data.remaining() < 4 {
return Err(NfsError::Xdr(format!("{label} bitmap length truncated")));
}
let count = data.get_u32() as usize;
if count > 16 || data.remaining() < count.saturating_mul(4) {
return Err(NfsError::Xdr(format!("{label} bitmap is invalid")));
}
let bitmap = (0..count).map(|_| data.get_u32()).collect();
if data.remaining() < 4 {
return Err(NfsError::Xdr(format!("{label} value length truncated")));
}
let length = data.get_u32() as usize;
if data.remaining() < length {
return Err(NfsError::Xdr(format!("{label} values truncated")));
}
Ok((bitmap, data.split_to(length)))
}
pub(crate) fn fattr4_has(bitmap: &[u32], attr_num: u32) -> bool {
bitmap_has(bitmap, attr_num)
}
fn skip_fixed(vals: &mut Bytes, n: usize, attr_name: &str) -> Result<()> {
if vals.remaining() < n {
return Err(NfsError::Xdr(format!(
"fattr4 {} truncated (need {} have {})",
attr_name,
n,
vals.remaining()
)));
}
vals.advance(n);
Ok(())
}
fn skip_settime4(vals: &mut Bytes, attr_name: &str) -> Result<()> {
if vals.remaining() < 4 {
return Err(NfsError::Xdr(format!(
"fattr4 {} settime4 truncated",
attr_name
)));
}
let how = vals.get_u32();
if how == 1 {
skip_fixed(vals, 12, attr_name)?;
}
Ok(())
}
fn decode_opaque(vals: &mut Bytes, attr_name: &str) -> Result<Bytes> {
if vals.remaining() < 4 {
return Err(NfsError::Xdr(format!(
"fattr4 {} opaque length truncated",
attr_name
)));
}
let len = vals.get_u32() as usize;
let padded = (len + 3) & !3;
if vals.remaining() < padded {
return Err(NfsError::Xdr(format!(
"fattr4 {} opaque data truncated (need {} have {})",
attr_name,
padded,
vals.remaining()
)));
}
let data = vals.slice(..len);
vals.advance(padded);
Ok(data)
}
fn skip_bitmap4(vals: &mut Bytes, attr_name: &str) -> Result<()> {
if vals.remaining() < 4 {
return Err(NfsError::Xdr(format!(
"fattr4 {} bitmap count truncated",
attr_name
)));
}
let count = vals.get_u32() as usize;
skip_fixed(vals, count * 4, attr_name)
}
fn skip_fs_locations(vals: &mut Bytes) -> Result<()> {
if vals.remaining() < 4 {
return Err(NfsError::Xdr(
"fattr4 fs_locations root truncated".to_string(),
));
}
let root_count = vals.get_u32() as usize;
for _ in 0..root_count {
let _ = decode_utf8str(vals)?;
}
if vals.remaining() < 4 {
return Err(NfsError::Xdr(
"fattr4 fs_locations locations count truncated".to_string(),
));
}
let loc_count = vals.get_u32() as usize;
for _ in 0..loc_count {
if vals.remaining() < 4 {
return Err(NfsError::Xdr(
"fattr4 fs_location4 server count truncated".to_string(),
));
}
let srv_count = vals.get_u32() as usize;
for _ in 0..srv_count {
let _ = decode_utf8str(vals)?;
}
if vals.remaining() < 4 {
return Err(NfsError::Xdr(
"fattr4 fs_location4 rootpath count truncated".to_string(),
));
}
let path_count = vals.get_u32() as usize;
for _ in 0..path_count {
let _ = decode_utf8str(vals)?;
}
}
Ok(())
}
pub(crate) fn decode_fattr4_to_attr(bitmap: &[u32], vals: &mut Bytes) -> Result<Attr> {
let mut attr = Attr::default();
if bitmap_has(bitmap, 0) {
skip_bitmap4(vals, "supported_attrs")?;
}
if bitmap_has(bitmap, 1) {
if vals.remaining() < 4 {
return Err(NfsError::Xdr("fattr4 type truncated".to_string()));
}
attr.type_ = vals.get_u32();
}
if bitmap_has(bitmap, 2) {
skip_fixed(vals, 4, "fh_expire_type")?;
}
if bitmap_has(bitmap, 3) {
skip_fixed(vals, 8, "change")?;
}
if bitmap_has(bitmap, 4) {
if vals.remaining() < 8 {
return Err(NfsError::Xdr("fattr4 size truncated".to_string()));
}
attr.filesize = vals.get_u64();
}
if bitmap_has(bitmap, 5) {
skip_fixed(vals, 4, "link_support")?;
}
if bitmap_has(bitmap, 6) {
skip_fixed(vals, 4, "symlink_support")?;
}
if bitmap_has(bitmap, 7) {
skip_fixed(vals, 4, "named_attr")?;
}
if bitmap_has(bitmap, 8) {
if vals.remaining() < 16 {
return Err(NfsError::Xdr("fattr4 fsid truncated".to_string()));
}
let major = vals.get_u64();
let _minor = vals.get_u64();
attr.fsid = major;
}
if bitmap_has(bitmap, 9) {
skip_fixed(vals, 4, "unique_handles")?;
}
if bitmap_has(bitmap, 10) {
skip_fixed(vals, 4, "lease_time")?;
}
if bitmap_has(bitmap, 11) {
if vals.remaining() < 4 {
return Err(NfsError::Xdr("fattr4 rdattr_error truncated".to_string()));
}
let code = vals.get_u32();
if code != 0 {
return Err(NfsError::RdattrError(code));
}
}
if bitmap_has(bitmap, attrnum::ACL) {
attr.acl = Some(decode_acl(vals)?);
}
if bitmap_has(bitmap, attrnum::ACLSUPPORT) {
skip_fixed(vals, 4, "aclsupport")?;
}
if bitmap_has(bitmap, 14) {
skip_fixed(vals, 4, "archive")?;
}
if bitmap_has(bitmap, 15) {
skip_fixed(vals, 4, "cansettime")?;
}
if bitmap_has(bitmap, 16) {
skip_fixed(vals, 4, "case_insensitive")?;
}
if bitmap_has(bitmap, 17) {
skip_fixed(vals, 4, "case_preserving")?;
}
if bitmap_has(bitmap, 18) {
skip_fixed(vals, 4, "chown_restricted")?;
}
if bitmap_has(bitmap, attrnum::FILEHANDLE) {
attr.filehandle = decode_opaque(vals, "filehandle")?;
}
if bitmap_has(bitmap, attrnum::FILEID) {
if vals.remaining() < 8 {
return Err(NfsError::Xdr("fattr4 fileid truncated".to_string()));
}
attr.fileid = vals.get_u64();
}
if bitmap_has(bitmap, 21) {
skip_fixed(vals, 8, "files_avail")?;
}
if bitmap_has(bitmap, 22) {
skip_fixed(vals, 8, "files_free")?;
}
if bitmap_has(bitmap, 23) {
skip_fixed(vals, 8, "files_total")?;
}
if bitmap_has(bitmap, 24) {
skip_fs_locations(vals)?;
}
if bitmap_has(bitmap, 25) {
skip_fixed(vals, 4, "hidden")?;
}
if bitmap_has(bitmap, 26) {
skip_fixed(vals, 4, "homogeneous")?;
}
if bitmap_has(bitmap, 27) {
skip_fixed(vals, 8, "maxfilesize")?;
}
if bitmap_has(bitmap, 28) {
skip_fixed(vals, 4, "maxlink")?;
}
if bitmap_has(bitmap, 29) {
skip_fixed(vals, 4, "maxname")?;
}
if bitmap_has(bitmap, 30) {
skip_fixed(vals, 8, "maxread")?;
}
if bitmap_has(bitmap, 31) {
skip_fixed(vals, 8, "maxwrite")?;
}
if bitmap_has(bitmap, 32) {
let _ = decode_utf8str(vals)?;
}
if bitmap_has(bitmap, attrnum::MODE) {
if vals.remaining() < 4 {
return Err(NfsError::Xdr("fattr4 mode truncated".to_string()));
}
attr.file_mode = vals.get_u32();
}
if bitmap_has(bitmap, 34) {
skip_fixed(vals, 4, "no_trunc")?;
}
if bitmap_has(bitmap, 35) {
if vals.remaining() < 4 {
return Err(NfsError::Xdr("fattr4 numlinks truncated".to_string()));
}
attr.nlink = vals.get_u32();
}
if bitmap_has(bitmap, 36) {
let owner_str = decode_utf8str(vals)?;
attr.uid = parse_numeric_owner(&owner_str);
attr.owner = owner_str;
}
if bitmap_has(bitmap, 37) {
let group_str = decode_utf8str(vals)?;
attr.gid = parse_numeric_owner(&group_str);
attr.owner_group = group_str;
}
if bitmap_has(bitmap, 38) {
skip_fixed(vals, 8, "quota_avail_hard")?;
}
if bitmap_has(bitmap, 39) {
skip_fixed(vals, 8, "quota_avail_soft")?;
}
if bitmap_has(bitmap, 40) {
skip_fixed(vals, 8, "quota_used")?;
}
if bitmap_has(bitmap, 41) {
if vals.remaining() < 8 {
return Err(NfsError::Xdr("fattr4 rawdev truncated".to_string()));
}
attr.spec_data[0] = vals.get_u32();
attr.spec_data[1] = vals.get_u32();
}
if bitmap_has(bitmap, 42) {
skip_fixed(vals, 8, "space_avail")?;
}
if bitmap_has(bitmap, 43) {
skip_fixed(vals, 8, "space_free")?;
}
if bitmap_has(bitmap, 44) {
skip_fixed(vals, 8, "space_total")?;
}
if bitmap_has(bitmap, 45) {
if vals.remaining() < 8 {
return Err(NfsError::Xdr("fattr4 space_used truncated".to_string()));
}
attr.used = vals.get_u64();
}
if bitmap_has(bitmap, 46) {
skip_fixed(vals, 4, "system")?;
}
if bitmap_has(bitmap, 47) {
attr.atime = decode_nfstime4(vals)?;
}
if bitmap_has(bitmap, 48) {
skip_settime4(vals, "time_access_set")?;
}
if bitmap_has(bitmap, 49) {
skip_fixed(vals, 12, "time_backup")?;
}
if bitmap_has(bitmap, 50) {
skip_fixed(vals, 12, "time_create")?;
}
if bitmap_has(bitmap, 51) {
skip_fixed(vals, 12, "time_delta")?;
}
if bitmap_has(bitmap, 52) {
attr.ctime = decode_nfstime4(vals)?;
}
if bitmap_has(bitmap, 53) {
attr.mtime = decode_nfstime4(vals)?;
}
if bitmap_has(bitmap, 54) {
skip_settime4(vals, "time_modify_set")?;
}
if bitmap_has(bitmap, 55) {
skip_fixed(vals, 8, "mounted_on_fileid")?;
}
if bitmap_has(bitmap, 56) {
skip_fixed(vals, 12, "dir_notif_delay")?;
}
if bitmap_has(bitmap, 57) {
skip_fixed(vals, 12, "dirent_notif_delay")?;
}
if bitmap_has(bitmap, 58) {
skip_acl(vals)?;
}
if bitmap_has(bitmap, 59) {
skip_acl(vals)?;
}
if bitmap_has(bitmap, 60) {
skip_fixed(vals, 16, "change_policy")?;
}
Ok(attr)
}
pub(super) fn decode_getattr_envelope(data: &mut Bytes) -> Result<(Vec<u32>, Bytes)> {
if data.remaining() < 4 {
return Err(NfsError::Xdr("GETATTR bitmap length truncated".to_string()));
}
let bitmap_len = data.get_u32() as usize;
if bitmap_len > 16 {
return Err(NfsError::Xdr(format!(
"GETATTR bitmap has {} words, max 16",
bitmap_len
)));
}
let mut bitmap = vec![0u32; bitmap_len];
for word in &mut bitmap {
if data.remaining() < 4 {
return Err(NfsError::Xdr("GETATTR bitmap word truncated".to_string()));
}
*word = data.get_u32();
}
if data.remaining() < 4 {
return Err(NfsError::Xdr(
"GETATTR attr_vals length truncated".to_string(),
));
}
let vals_len = data.get_u32() as usize;
if data.remaining() < vals_len {
return Err(NfsError::Xdr("GETATTR attr_vals truncated".to_string()));
}
let vals = data.split_to(vals_len);
let pad = (4 - vals_len % 4) % 4;
if data.remaining() >= pad {
data.advance(pad);
}
Ok((bitmap, vals))
}
pub(crate) fn decode_getattr_response(data: &mut Bytes) -> Result<Attr> {
let (bitmap, mut vals) = decode_getattr_envelope(data)?;
let result = decode_fattr4_to_attr(&bitmap, &mut vals);
if let Err(ref e) = result {
let set_bits: Vec<u32> = (0u32..((bitmap.len() as u32) * 32))
.filter(|&bit| bitmap_has(&bitmap, bit))
.collect();
tracing::warn!(
"GETATTR decode failed: {} (bitmap={:?} set_attrs={:?} vals_len={})",
e,
bitmap,
set_bits,
vals.len()
);
}
result
}
fn decode_nfstime4(buf: &mut Bytes) -> Result<Time> {
if buf.remaining() < 12 {
return Err(NfsError::Xdr("nfstime4 truncated".to_string()));
}
let seconds = buf.get_i64();
let nseconds = buf.get_u32();
let clamped = if seconds < 0 {
0u32
} else if seconds > u32::MAX as i64 {
u32::MAX
} else {
seconds as u32
};
Ok(Time {
seconds: clamped,
nseconds,
})
}
pub(super) fn decode_utf8str(buf: &mut Bytes) -> Result<String> {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("utf8str length truncated".to_string()));
}
let len = buf.get_u32() as usize;
let padded = (len + 3) & !3;
if buf.remaining() < padded {
return Err(NfsError::Xdr("utf8str data truncated".to_string()));
}
let bytes = buf.slice(..len);
buf.advance(padded);
String::from_utf8(bytes.to_vec())
.map_err(|_| NfsError::Xdr("invalid UTF-8 in string attribute".to_string()))
}
fn parse_numeric_owner(s: &str) -> u32 {
if let Ok(n) = s.parse::<u32>() {
return n;
}
if let Some(name) = s.split('@').next() {
if let Ok(n) = name.parse::<u32>() {
return n;
}
if name == "root" {
return 0;
}
}
65534
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn standard_bitmap_has_expected_bits() {
let bm = standard_getattr_bitmap();
assert!(bitmap_has(&bm, 1)); assert!(bitmap_has(&bm, 4)); assert!(bitmap_has(&bm, 8)); assert!(bitmap_has(&bm, 12)); assert!(bitmap_has(&bm, 19)); assert!(bitmap_has(&bm, 20)); assert!(bitmap_has(&bm, 33)); assert!(bitmap_has(&bm, 35)); assert!(bitmap_has(&bm, 36)); assert!(bitmap_has(&bm, 37)); assert!(bitmap_has(&bm, 41)); assert!(bitmap_has(&bm, 45)); assert!(bitmap_has(&bm, 47)); assert!(bitmap_has(&bm, 52)); assert!(bitmap_has(&bm, 53)); assert!(!bitmap_has(&bm, 0)); assert!(!bitmap_has(&bm, 3)); assert!(!bitmap_has(&bm, 34)); }
#[test]
fn parse_numeric_owner_values() {
assert_eq!(parse_numeric_owner("1000"), 1000);
assert_eq!(parse_numeric_owner("0"), 0);
assert_eq!(parse_numeric_owner("1000@example.com"), 1000);
assert_eq!(parse_numeric_owner("root"), 0);
assert_eq!(parse_numeric_owner("root@netapp.com"), 0);
assert_eq!(parse_numeric_owner("nobody@localdomain"), 65534);
}
#[test]
fn parse_numeric_owner_edge_cases() {
assert_eq!(parse_numeric_owner("65534"), 65534);
assert_eq!(parse_numeric_owner("4294967295"), 4294967295); assert_eq!(parse_numeric_owner(""), 65534);
assert_eq!(parse_numeric_owner("@domain"), 65534);
}
#[test]
fn bitmap_has_empty() {
let bm: [u32; 0] = [];
assert!(!bitmap_has(&bm, 0));
assert!(!bitmap_has(&bm, 32));
assert!(!bitmap_has(&bm, 100));
}
#[test]
fn bitmap_has_single_word() {
let bm = [0x00000002u32]; assert!(!bitmap_has(&bm, 0));
assert!(bitmap_has(&bm, 1));
assert!(!bitmap_has(&bm, 2));
assert!(!bitmap_has(&bm, 32)); }
#[test]
fn bitmap_has_two_words() {
let bm = [0u32, 0x00000001]; assert!(!bitmap_has(&bm, 0));
assert!(!bitmap_has(&bm, 31));
assert!(bitmap_has(&bm, 32));
assert!(!bitmap_has(&bm, 33));
}
fn build_fattr4_type_only(ftype: u32) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&1u32.to_be_bytes()); buf.extend_from_slice(&0x00000002u32.to_be_bytes()); let vals = ftype.to_be_bytes();
buf.extend_from_slice(&(vals.len() as u32).to_be_bytes());
buf.extend_from_slice(&vals);
buf
}
#[test]
fn decode_getattr_type_only() {
let buf = build_fattr4_type_only(1); let mut bytes = Bytes::from(buf);
let attr = decode_getattr_response(&mut bytes).unwrap();
assert_eq!(attr.type_, 1);
assert_eq!(attr.filesize, 0); }
#[test]
fn decode_getattr_type_directory() {
let buf = build_fattr4_type_only(2); let mut bytes = Bytes::from(buf);
let attr = decode_getattr_response(&mut bytes).unwrap();
assert_eq!(attr.type_, 2);
}
#[test]
fn decode_fattr4_size_and_fileid() {
let bitmap = [(1u32 << 4) | (1 << 20)];
let mut vals = Vec::new();
vals.extend_from_slice(&12345u64.to_be_bytes()); vals.extend_from_slice(&99u64.to_be_bytes()); let mut v = Bytes::from(vals);
let attr = decode_fattr4_to_attr(&bitmap, &mut v).unwrap();
assert_eq!(attr.filesize, 12345);
assert_eq!(attr.fileid, 99);
}
#[test]
fn decode_fattr4_mode_and_nlinks() {
let bitmap = [0u32, (1 << 1) | (1 << 3)];
let mut vals = Vec::new();
vals.extend_from_slice(&0o755u32.to_be_bytes()); vals.extend_from_slice(&3u32.to_be_bytes()); let mut v = Bytes::from(vals);
let attr = decode_fattr4_to_attr(&bitmap, &mut v).unwrap();
assert_eq!(attr.file_mode, 0o755);
assert_eq!(attr.nlink, 3);
}
#[test]
fn decode_fattr4_time_access() {
let bitmap = [0u32, 1 << 15];
let mut vals = Vec::new();
vals.extend_from_slice(&1700000000i64.to_be_bytes()); vals.extend_from_slice(&123456789u32.to_be_bytes()); let mut v = Bytes::from(vals);
let attr = decode_fattr4_to_attr(&bitmap, &mut v).unwrap();
assert_eq!(attr.atime.nseconds, 123456789);
}
#[test]
fn decode_getattr_empty_bitmap() {
let mut buf = Vec::new();
buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); let mut bytes = Bytes::from(buf);
let attr = decode_getattr_response(&mut bytes).unwrap();
assert_eq!(attr, Attr::default());
}
#[test]
fn decode_getattr_truncated_bitmap() {
let buf = vec![0u8; 2]; let mut bytes = Bytes::from(buf);
assert!(decode_getattr_response(&mut bytes).is_err());
}
#[test]
fn decode_nfstime4_valid() {
let mut buf = Vec::new();
buf.extend_from_slice(&1000i64.to_be_bytes());
buf.extend_from_slice(&500u32.to_be_bytes());
let mut bytes = Bytes::from(buf);
let t = decode_nfstime4(&mut bytes).unwrap();
assert_eq!(t.seconds, 1000);
assert_eq!(t.nseconds, 500);
}
#[test]
fn decode_nfsv41_readdirplus_response() {
let bitmap_word0: u32 = (1 << 1) | (1 << 4) | (1 << 8) | (1 << 12) | (1 << 19); let bitmap_word1: u32 = (1 << 2) | (1 << 3) | (1 << 4) | (1 << 12) | (1 << 19) | (1 << 20); let bitmap = [bitmap_word0, bitmap_word1];
let hex: Vec<u8> = vec![
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x01, 0xe7, 0x00, 0x00,
0x00, 0x06, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x40, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0xa1, 0x00, 0x00,
0x00, 0x06, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x40, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0xa1, 0x00, 0x00,
0x00, 0x09, 0x45, 0x56, 0x45, 0x52, 0x59, 0x4f, 0x4e, 0x45, 0x40, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x14, 0x01, 0x00, 0x01, 0x81, 0x00, 0x00, 0x00, 0x00, 0x75, 0xd0,
0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0x71, 0x8b, 0x63, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x31, 0x40, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x42, 0x40,
0x00, 0x00, 0x00, 0x00, 0x69, 0xd0, 0xa5, 0x3a, 0x00, 0x00, 0x9a, 0x36,
];
assert_eq!(hex.len(), 180);
let mut vals = Bytes::from(hex);
let attr = decode_fattr4_to_attr(&bitmap, &mut vals).unwrap();
assert_eq!(vals.remaining(), 0, "all bytes consumed");
assert_eq!(attr.type_, 2); assert_eq!(attr.filesize, 81);
assert_eq!(attr.fsid, 0);
assert_eq!(attr.acl.as_ref().map(|a| a.aces.len()), Some(3));
assert_eq!(attr.filehandle.len(), 20);
assert_eq!(attr.nlink, 8);
assert_eq!(attr.owner, "0");
assert_eq!(attr.uid, 0);
assert_eq!(attr.ctime.seconds, 0x69d0a53a); assert_eq!(attr.ctime.nseconds, 0x9a36);
}
#[test]
fn decode_nfstime4_truncated() {
let buf = vec![0u8; 8]; let mut bytes = Bytes::from(buf);
assert!(decode_nfstime4(&mut bytes).is_err());
}
}