Skip to main content

libpna/entry/
meta.rs

1//! Metadata and permission types for archive entries.
2
3use crate::util::bounded::{LengthExceeded, str::BoundedString};
4use crate::{Duration, UnknownValueError};
5use crate::{RawChunk, entry::ExtendedAttribute};
6use std::io::{self, Read};
7use std::ops::Deref;
8use std::str;
9
10/// Metadata information about an entry.
11/// # Examples
12/// ```rust
13/// # use std::time::SystemTimeError;
14/// # fn main() -> Result<(), SystemTimeError> {
15/// use libpna::{Duration, Metadata};
16///
17/// let since_unix_epoch = Duration::seconds(1000);
18/// let metadata = Metadata::new()
19///     .with_accessed(Some(since_unix_epoch))
20///     .with_created(Some(since_unix_epoch))
21///     .with_modified(Some(since_unix_epoch));
22/// # Ok(())
23/// # }
24/// ```
25#[allow(deprecated)]
26#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
27pub struct Metadata {
28    pub(crate) raw_file_size: Option<u128>,
29    pub(crate) compressed_size: usize,
30    pub(crate) created: Option<Duration>,
31    pub(crate) modified: Option<Duration>,
32    pub(crate) accessed: Option<Duration>,
33    pub(crate) permission: Option<Permission>,
34    pub(crate) link_target_type: Option<LinkTargetType>,
35    pub(crate) owner_uid: Option<OwnerUid>,
36    pub(crate) owner_gid: Option<OwnerGid>,
37    pub(crate) owner_user_name: Option<OwnerUserName>,
38    pub(crate) owner_group_name: Option<OwnerGroupName>,
39    pub(crate) owner_user_sid: Option<OwnerUserSid>,
40    pub(crate) owner_group_sid: Option<OwnerGroupSid>,
41    pub(crate) permission_mode: Option<PermissionMode>,
42    pub(crate) xattrs: Vec<ExtendedAttribute>,
43}
44
45/// Attributes known before streaming an entry payload.
46///
47/// This type combines structured [`Metadata`] with raw extra chunks. Extra
48/// chunks are written in insertion order immediately after the entry header,
49/// matching the placement used by entry builders. Extra chunk types are not
50/// validated; even structural or critical chunks are written as supplied.
51///
52/// Streaming entry methods do not generate an `fSIZ` chunk from [`Metadata`]
53/// size fields. A caller-supplied raw `fSIZ` extra chunk is still written like
54/// any other extra chunk.
55pub struct EntryWriteAttributes {
56    pub(crate) metadata: Metadata,
57    pub(crate) extra_chunks: Vec<RawChunk>,
58}
59
60impl EntryWriteAttributes {
61    /// Creates streaming entry attributes from structured metadata.
62    #[inline]
63    pub fn new(metadata: Metadata) -> Self {
64        Self {
65            metadata,
66            extra_chunks: Vec::new(),
67        }
68    }
69
70    /// Returns the structured metadata.
71    #[inline]
72    pub const fn metadata(&self) -> &Metadata {
73        &self.metadata
74    }
75
76    /// Adds a raw extra chunk, preserving insertion order.
77    #[inline]
78    pub fn add_extra_chunk<T>(&mut self, chunk: T) -> &mut Self
79    where
80        T: Into<RawChunk>,
81    {
82        self.extra_chunks.push(chunk.into());
83        self
84    }
85}
86
87impl From<Metadata> for EntryWriteAttributes {
88    #[inline]
89    fn from(metadata: Metadata) -> Self {
90        Self::new(metadata)
91    }
92}
93
94impl Metadata {
95    /// Creates a new [`Metadata`].
96    #[inline]
97    pub const fn new() -> Self {
98        Self {
99            raw_file_size: Some(0),
100            compressed_size: 0,
101            created: None,
102            modified: None,
103            accessed: None,
104            permission: None,
105            link_target_type: None,
106            owner_uid: None,
107            owner_gid: None,
108            owner_user_name: None,
109            owner_group_name: None,
110            owner_user_sid: None,
111            owner_group_sid: None,
112            permission_mode: None,
113            xattrs: Vec::new(),
114        }
115    }
116
117    /// Sets the created time as the duration since the Unix epoch.
118    ///
119    /// # Examples
120    /// ```rust
121    /// # use std::time::SystemTimeError;
122    /// # fn main() -> Result<(), SystemTimeError> {
123    /// use libpna::{Duration, Metadata};
124    ///
125    /// let since_unix_epoch = Duration::seconds(1000);
126    /// let metadata = Metadata::new().with_created(Some(since_unix_epoch));
127    /// # Ok(())
128    /// # }
129    /// ```
130    #[inline]
131    pub const fn with_created(mut self, created: Option<Duration>) -> Self {
132        self.created = created;
133        self
134    }
135
136    /// Sets the modified time as the duration since the Unix epoch.
137    ///
138    /// # Examples
139    /// ```rust
140    /// # use std::time::SystemTimeError;
141    /// # fn main() -> Result<(), SystemTimeError> {
142    /// use libpna::{Duration, Metadata};
143    ///
144    /// let since_unix_epoch = Duration::seconds(1000);
145    /// let metadata = Metadata::new().with_modified(Some(since_unix_epoch));
146    /// # Ok(())
147    /// # }
148    /// ```
149    #[inline]
150    pub const fn with_modified(mut self, modified: Option<Duration>) -> Self {
151        self.modified = modified;
152        self
153    }
154
155    /// Sets the accessed time as the duration since the Unix epoch.
156    ///
157    /// # Examples
158    /// ```rust
159    /// # use std::time::SystemTimeError;
160    /// # fn main() -> Result<(), SystemTimeError> {
161    /// use libpna::{Duration, Metadata};
162    ///
163    /// let since_unix_epoch = Duration::seconds(1000);
164    /// let metadata = Metadata::new().with_accessed(Some(since_unix_epoch));
165    /// # Ok(())
166    /// # }
167    /// ```
168    #[inline]
169    pub const fn with_accessed(mut self, accessed: Option<Duration>) -> Self {
170        self.accessed = accessed;
171        self
172    }
173
174    /// Sets the permission of the entry.
175    #[deprecated(
176        since = "0.34.0",
177        note = "the fPRM chunk is superseded by the owner facet chunks; use Metadata::with_owner_uid/with_owner_gid/with_owner_user_name/with_owner_group_name/with_owner_user_sid/with_owner_group_sid/with_permission_mode"
178    )]
179    #[allow(deprecated)]
180    #[inline]
181    pub fn with_permission(mut self, permission: Option<Permission>) -> Self {
182        self.permission = permission;
183        self
184    }
185
186    /// Sets the owner user id facet (`fUId`).
187    #[inline]
188    pub fn with_owner_uid(mut self, value: Option<OwnerUid>) -> Self {
189        self.owner_uid = value;
190        self
191    }
192    /// Sets the owner group id facet (`fGId`).
193    #[inline]
194    pub fn with_owner_gid(mut self, value: Option<OwnerGid>) -> Self {
195        self.owner_gid = value;
196        self
197    }
198    /// Sets the owner user name facet (`fONm`).
199    #[inline]
200    pub fn with_owner_user_name(mut self, value: Option<OwnerUserName>) -> Self {
201        self.owner_user_name = value;
202        self
203    }
204    /// Sets the owner group name facet (`fGNm`).
205    #[inline]
206    pub fn with_owner_group_name(mut self, value: Option<OwnerGroupName>) -> Self {
207        self.owner_group_name = value;
208        self
209    }
210    /// Sets the owner user SID facet (`fOSi`).
211    #[inline]
212    pub fn with_owner_user_sid(mut self, value: Option<OwnerUserSid>) -> Self {
213        self.owner_user_sid = value;
214        self
215    }
216    /// Sets the owner group SID facet (`fGSi`).
217    #[inline]
218    pub fn with_owner_group_sid(mut self, value: Option<OwnerGroupSid>) -> Self {
219        self.owner_group_sid = value;
220        self
221    }
222    /// Sets the POSIX permission mode facet (`fMOd`).
223    #[inline]
224    pub fn with_permission_mode(mut self, value: Option<PermissionMode>) -> Self {
225        self.permission_mode = value;
226        self
227    }
228
229    /// Sets the link target type of the entry.
230    /// Only meaningful for symbolic link and hard link entries.
231    #[inline]
232    pub const fn with_link_target_type(mut self, link_target_type: Option<LinkTargetType>) -> Self {
233        self.link_target_type = link_target_type;
234        self
235    }
236
237    /// Sets the extended attributes facet (`xATR`).
238    ///
239    /// Each attribute is serialized as one `xATR` chunk. Passing an empty
240    /// collection records no attributes and emits no `xATR` chunks.
241    #[inline]
242    pub fn with_xattrs(mut self, xattrs: impl Into<Vec<ExtendedAttribute>>) -> Self {
243        self.xattrs = xattrs.into();
244        self
245    }
246
247    /// Returns the raw file size of this entry's data in bytes.
248    #[inline]
249    pub const fn raw_file_size(&self) -> Option<u128> {
250        self.raw_file_size
251    }
252    /// Returns the compressed size of this entry's data in bytes.
253    #[inline]
254    pub const fn compressed_size(&self) -> usize {
255        self.compressed_size
256    }
257    /// Returns the created time since the Unix epoch for the entry.
258    #[inline]
259    pub const fn created(&self) -> Option<Duration> {
260        self.created
261    }
262    /// Returns the modified time since the Unix epoch for the entry.
263    #[inline]
264    pub const fn modified(&self) -> Option<Duration> {
265        self.modified
266    }
267    /// Returns the accessed time since the Unix epoch for the entry.
268    #[inline]
269    pub const fn accessed(&self) -> Option<Duration> {
270        self.accessed
271    }
272    /// Returns the owner, group, and permission bits for the entry.
273    #[deprecated(
274        since = "0.34.0",
275        note = "the fPRM chunk is superseded by the owner facet chunks; use Metadata::owner_uid/owner_gid/owner_user_name/owner_group_name/owner_user_sid/owner_group_sid/permission_mode"
276    )]
277    #[allow(deprecated)]
278    #[inline]
279    pub const fn permission(&self) -> Option<&Permission> {
280        self.permission.as_ref()
281    }
282    /// Returns the owner user id facet (`fUId`), if recorded.
283    #[inline]
284    pub const fn owner_uid(&self) -> Option<OwnerUid> {
285        self.owner_uid
286    }
287    /// Returns the owner group id facet (`fGId`), if recorded.
288    #[inline]
289    pub const fn owner_gid(&self) -> Option<OwnerGid> {
290        self.owner_gid
291    }
292    /// Returns the owner user name facet (`fONm`), if recorded.
293    #[inline]
294    pub fn owner_user_name(&self) -> Option<&OwnerUserName> {
295        self.owner_user_name.as_ref()
296    }
297    /// Returns the owner group name facet (`fGNm`), if recorded.
298    #[inline]
299    pub fn owner_group_name(&self) -> Option<&OwnerGroupName> {
300        self.owner_group_name.as_ref()
301    }
302    /// Returns the owner user SID facet (`fOSi`), if recorded.
303    #[inline]
304    pub fn owner_user_sid(&self) -> Option<&OwnerUserSid> {
305        self.owner_user_sid.as_ref()
306    }
307    /// Returns the owner group SID facet (`fGSi`), if recorded.
308    #[inline]
309    pub fn owner_group_sid(&self) -> Option<&OwnerGroupSid> {
310        self.owner_group_sid.as_ref()
311    }
312    /// Returns the POSIX permission mode facet (`fMOd`), if recorded.
313    #[inline]
314    pub const fn permission_mode(&self) -> Option<PermissionMode> {
315        self.permission_mode
316    }
317
318    /// Returns the link target type for this entry, if present.
319    ///
320    /// - `None`: fLTP chunk was absent.
321    /// - `Some(Unknown)`: fLTP chunk present but target type undetermined.
322    /// - `Some(File)` / `Some(Directory)`: known target type.
323    #[inline]
324    pub const fn link_target_type(&self) -> Option<LinkTargetType> {
325        self.link_target_type
326    }
327
328    /// Returns the extended attributes (`xATR`) recorded for this entry.
329    #[inline]
330    pub fn xattrs(&self) -> &[ExtendedAttribute] {
331        &self.xattrs
332    }
333}
334
335impl Default for Metadata {
336    #[inline]
337    fn default() -> Self {
338        Self::new()
339    }
340}
341
342/// Owner, group, and permission bits for an archive entry.
343#[deprecated(
344    since = "0.34.0",
345    note = "the fPRM chunk is superseded by the owner facet chunks; use the owner facet API (Metadata::owner_uid/owner_gid/owner_user_name/owner_group_name/owner_user_sid/owner_group_sid/permission_mode and the matching Metadata::with_* setters)"
346)]
347#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
348pub struct Permission {
349    uid: u64,
350    uname: String,
351    gid: u64,
352    gname: String,
353    permission: u16,
354}
355
356#[allow(deprecated)]
357impl Permission {
358    /// Creates a new [`Permission`] with the given user, group, and permission bits.
359    ///
360    /// The `uid`/`gid` are numeric POSIX IDs, `uname`/`gname` are the
361    /// corresponding names, and `permission` holds the file mode bits (e.g. `0o755`).
362    ///
363    /// # Examples
364    ///
365    /// ```
366    /// # #![allow(deprecated)]
367    /// use libpna::Permission;
368    ///
369    /// let perm = Permission::new(1000, "user".into(), 100, "group".into(), 0o755);
370    /// ```
371    #[deprecated(
372        since = "0.34.0",
373        note = "the fPRM chunk is superseded by the owner facet chunks; use the owner facet API (Metadata::owner_uid/owner_gid/owner_user_name/owner_group_name/owner_user_sid/owner_group_sid/permission_mode and the matching Metadata::with_* setters)"
374    )]
375    #[inline]
376    pub const fn new(uid: u64, uname: String, gid: u64, gname: String, permission: u16) -> Self {
377        Self {
378            uid,
379            uname,
380            gid,
381            gname,
382            permission,
383        }
384    }
385    /// Returns the user ID associated with this permission.
386    ///
387    /// # Examples
388    ///
389    /// ```
390    /// # #![allow(deprecated)]
391    /// use libpna::Permission;
392    ///
393    /// let perm = Permission::new(1000, "user1".into(), 100, "group1".into(), 0o644);
394    /// assert_eq!(perm.uid(), 1000);
395    /// ```
396    #[deprecated(
397        since = "0.34.0",
398        note = "the fPRM chunk is superseded by the owner facet chunks; use the owner facet API (Metadata::owner_uid/owner_gid/owner_user_name/owner_group_name/owner_user_sid/owner_group_sid/permission_mode and the matching Metadata::with_* setters)"
399    )]
400    #[inline]
401    pub const fn uid(&self) -> u64 {
402        self.uid
403    }
404
405    /// Returns the user name associated with this permission.
406    ///
407    /// # Examples
408    ///
409    /// ```
410    /// # #![allow(deprecated)]
411    /// use libpna::Permission;
412    ///
413    /// let perm = Permission::new(1000, "user1".into(), 100, "group1".into(), 0o644);
414    /// assert_eq!(perm.uname(), "user1");
415    /// ```
416    #[deprecated(
417        since = "0.34.0",
418        note = "the fPRM chunk is superseded by the owner facet chunks; use the owner facet API (Metadata::owner_uid/owner_gid/owner_user_name/owner_group_name/owner_user_sid/owner_group_sid/permission_mode and the matching Metadata::with_* setters)"
419    )]
420    #[inline]
421    pub fn uname(&self) -> &str {
422        &self.uname
423    }
424
425    /// Returns the group ID associated with this permission.
426    ///
427    /// # Examples
428    ///
429    /// ```
430    /// # #![allow(deprecated)]
431    /// use libpna::Permission;
432    ///
433    /// let perm = Permission::new(1000, "user1".into(), 100, "group1".into(), 0o644);
434    /// assert_eq!(perm.gid(), 100);
435    /// ```
436    #[deprecated(
437        since = "0.34.0",
438        note = "the fPRM chunk is superseded by the owner facet chunks; use the owner facet API (Metadata::owner_uid/owner_gid/owner_user_name/owner_group_name/owner_user_sid/owner_group_sid/permission_mode and the matching Metadata::with_* setters)"
439    )]
440    #[inline]
441    pub const fn gid(&self) -> u64 {
442        self.gid
443    }
444
445    /// Returns the group name associated with this permission.
446    ///
447    /// # Examples
448    ///
449    /// ```
450    /// # #![allow(deprecated)]
451    /// use libpna::Permission;
452    ///
453    /// let perm = Permission::new(1000, "user1".into(), 100, "group1".into(), 0o644);
454    /// assert_eq!(perm.gname(), "group1");
455    /// ```
456    #[deprecated(
457        since = "0.34.0",
458        note = "the fPRM chunk is superseded by the owner facet chunks; use the owner facet API (Metadata::owner_uid/owner_gid/owner_user_name/owner_group_name/owner_user_sid/owner_group_sid/permission_mode and the matching Metadata::with_* setters)"
459    )]
460    #[inline]
461    pub fn gname(&self) -> &str {
462        &self.gname
463    }
464
465    /// Returns the permission bits associated with this permission.
466    ///
467    /// # Examples
468    ///
469    /// ```
470    /// # #![allow(deprecated)]
471    /// use libpna::Permission;
472    ///
473    /// let perm = Permission::new(1000, "user1".into(), 100, "group1".into(), 0o644);
474    /// assert_eq!(perm.permissions(), 0o644);
475    /// ```
476    #[deprecated(
477        since = "0.34.0",
478        note = "the fPRM chunk is superseded by the owner facet chunks; use the owner facet API (Metadata::owner_uid/owner_gid/owner_user_name/owner_group_name/owner_user_sid/owner_group_sid/permission_mode and the matching Metadata::with_* setters)"
479    )]
480    #[inline]
481    pub const fn permissions(&self) -> u16 {
482        self.permission
483    }
484
485    pub(crate) fn to_bytes(&self) -> Vec<u8> {
486        let mut bytes = Vec::with_capacity(20 + self.uname.len() + self.gname.len());
487        bytes.extend_from_slice(&self.uid.to_be_bytes());
488        bytes.extend_from_slice(&(self.uname.len() as u8).to_be_bytes());
489        bytes.extend_from_slice(self.uname.as_bytes());
490        bytes.extend_from_slice(&self.gid.to_be_bytes());
491        bytes.extend_from_slice(&(self.gname.len() as u8).to_be_bytes());
492        bytes.extend_from_slice(self.gname.as_bytes());
493        bytes.extend_from_slice(&self.permission.to_be_bytes());
494        bytes
495    }
496
497    pub(crate) fn try_from_bytes(mut bytes: &[u8]) -> io::Result<Self> {
498        let uid = u64::from_be_bytes({
499            let mut buf = [0; 8];
500            bytes.read_exact(&mut buf)?;
501            buf
502        });
503        let uname_len = {
504            let mut buf = [0; 1];
505            bytes.read_exact(&mut buf)?;
506            buf[0] as usize
507        };
508        let uname = String::from_utf8({
509            let mut buf = vec![0; uname_len];
510            bytes.read_exact(&mut buf)?;
511            buf
512        })
513        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
514        let gid = u64::from_be_bytes({
515            let mut buf = [0; 8];
516            bytes.read_exact(&mut buf)?;
517            buf
518        });
519        let gname_len = {
520            let mut buf = [0; 1];
521            bytes.read_exact(&mut buf)?;
522            buf[0] as usize
523        };
524        let gname = String::from_utf8({
525            let mut buf = vec![0; gname_len];
526            bytes.read_exact(&mut buf)?;
527            buf
528        })
529        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
530        let permission = u16::from_be_bytes({
531            let mut buf = [0; 2];
532            bytes.read_exact(&mut buf)?;
533            buf
534        });
535        Ok(Self {
536            uid,
537            uname,
538            gid,
539            gname,
540            permission,
541        })
542    }
543}
544
545/// Maximum owner-facet string byte length (the `fONm`/`fGNm`/`fOSi`/`fGSi`
546/// chunk Body uses a 1-byte length prefix).
547const OWNER_STR_MAX: usize = u8::MAX as usize;
548
549/// Owner user name (`fONm`).
550#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
551#[repr(transparent)]
552pub struct OwnerUserName(BoundedString<OWNER_STR_MAX>);
553
554impl OwnerUserName {
555    /// Constructs an [`OwnerUserName`].
556    ///
557    /// # Errors
558    ///
559    /// Returns [`LengthExceeded`] when the byte length exceeds 255.
560    #[inline]
561    pub fn new(value: impl Into<Box<str>>) -> Result<Self, LengthExceeded> {
562        BoundedString::new(value).map(Self)
563    }
564    /// Returns the name as a string slice.
565    #[inline]
566    #[must_use]
567    pub fn as_str(&self) -> &str {
568        self.0.as_str()
569    }
570    pub(crate) fn to_bytes(&self) -> Vec<u8> {
571        let b = self.0.as_str().as_bytes();
572        let mut v = Vec::with_capacity(1 + b.len());
573        // Type guarantees b.len() <= 255 (BoundedString<255> invariant).
574        v.push(b.len() as u8);
575        v.extend_from_slice(b);
576        v
577    }
578    pub(crate) fn try_from_bytes(bytes: &[u8]) -> io::Result<Self> {
579        let (&len, rest) = bytes.split_first().ok_or(io::ErrorKind::UnexpectedEof)?;
580        let s = rest
581            .get(..len as usize)
582            .ok_or(io::ErrorKind::UnexpectedEof)?;
583        let s = str::from_utf8(s).map_err(|_| io::ErrorKind::InvalidData)?;
584        Self::new(s.to_owned()).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
585    }
586}
587
588impl Deref for OwnerUserName {
589    type Target = str;
590    #[inline]
591    fn deref(&self) -> &str {
592        self.0.as_str()
593    }
594}
595impl TryFrom<String> for OwnerUserName {
596    type Error = LengthExceeded;
597    #[inline]
598    fn try_from(value: String) -> Result<Self, Self::Error> {
599        Self::new(value)
600    }
601}
602impl TryFrom<&str> for OwnerUserName {
603    type Error = LengthExceeded;
604    #[inline]
605    fn try_from(value: &str) -> Result<Self, Self::Error> {
606        Self::new(value)
607    }
608}
609impl From<OwnerUserName> for String {
610    #[inline]
611    fn from(value: OwnerUserName) -> Self {
612        value.0.into()
613    }
614}
615
616/// Owner group name (`fGNm`).
617#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
618#[repr(transparent)]
619pub struct OwnerGroupName(BoundedString<OWNER_STR_MAX>);
620
621impl OwnerGroupName {
622    /// Constructs an [`OwnerGroupName`].
623    ///
624    /// # Errors
625    ///
626    /// Returns [`LengthExceeded`] when the byte length exceeds 255.
627    #[inline]
628    pub fn new(value: impl Into<Box<str>>) -> Result<Self, LengthExceeded> {
629        BoundedString::new(value).map(Self)
630    }
631    /// Returns the name as a string slice.
632    #[inline]
633    #[must_use]
634    pub fn as_str(&self) -> &str {
635        self.0.as_str()
636    }
637    pub(crate) fn to_bytes(&self) -> Vec<u8> {
638        let b = self.0.as_str().as_bytes();
639        let mut v = Vec::with_capacity(1 + b.len());
640        // Type guarantees b.len() <= 255 (BoundedString<255> invariant).
641        v.push(b.len() as u8);
642        v.extend_from_slice(b);
643        v
644    }
645    pub(crate) fn try_from_bytes(bytes: &[u8]) -> io::Result<Self> {
646        let (&len, rest) = bytes.split_first().ok_or(io::ErrorKind::UnexpectedEof)?;
647        let s = rest
648            .get(..len as usize)
649            .ok_or(io::ErrorKind::UnexpectedEof)?;
650        let s = str::from_utf8(s).map_err(|_| io::ErrorKind::InvalidData)?;
651        Self::new(s.to_owned()).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
652    }
653}
654
655impl Deref for OwnerGroupName {
656    type Target = str;
657    #[inline]
658    fn deref(&self) -> &str {
659        self.0.as_str()
660    }
661}
662impl TryFrom<String> for OwnerGroupName {
663    type Error = LengthExceeded;
664    #[inline]
665    fn try_from(value: String) -> Result<Self, Self::Error> {
666        Self::new(value)
667    }
668}
669impl TryFrom<&str> for OwnerGroupName {
670    type Error = LengthExceeded;
671    #[inline]
672    fn try_from(value: &str) -> Result<Self, Self::Error> {
673        Self::new(value)
674    }
675}
676impl From<OwnerGroupName> for String {
677    #[inline]
678    fn from(value: OwnerGroupName) -> Self {
679        value.0.into()
680    }
681}
682
683/// Owner user SID (`fOSi`).
684#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
685#[repr(transparent)]
686pub struct OwnerUserSid(BoundedString<OWNER_STR_MAX>);
687
688impl OwnerUserSid {
689    /// Constructs an [`OwnerUserSid`].
690    ///
691    /// # Errors
692    ///
693    /// Returns [`LengthExceeded`] when the byte length exceeds 255.
694    #[inline]
695    pub fn new(value: impl Into<Box<str>>) -> Result<Self, LengthExceeded> {
696        BoundedString::new(value).map(Self)
697    }
698    /// Returns the name as a string slice.
699    #[inline]
700    #[must_use]
701    pub fn as_str(&self) -> &str {
702        self.0.as_str()
703    }
704    pub(crate) fn to_bytes(&self) -> Vec<u8> {
705        let b = self.0.as_str().as_bytes();
706        let mut v = Vec::with_capacity(1 + b.len());
707        // Type guarantees b.len() <= 255 (BoundedString<255> invariant).
708        v.push(b.len() as u8);
709        v.extend_from_slice(b);
710        v
711    }
712    pub(crate) fn try_from_bytes(bytes: &[u8]) -> io::Result<Self> {
713        let (&len, rest) = bytes.split_first().ok_or(io::ErrorKind::UnexpectedEof)?;
714        let s = rest
715            .get(..len as usize)
716            .ok_or(io::ErrorKind::UnexpectedEof)?;
717        let s = str::from_utf8(s).map_err(|_| io::ErrorKind::InvalidData)?;
718        Self::new(s.to_owned()).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
719    }
720}
721
722impl Deref for OwnerUserSid {
723    type Target = str;
724    #[inline]
725    fn deref(&self) -> &str {
726        self.0.as_str()
727    }
728}
729impl TryFrom<String> for OwnerUserSid {
730    type Error = LengthExceeded;
731    #[inline]
732    fn try_from(value: String) -> Result<Self, Self::Error> {
733        Self::new(value)
734    }
735}
736impl TryFrom<&str> for OwnerUserSid {
737    type Error = LengthExceeded;
738    #[inline]
739    fn try_from(value: &str) -> Result<Self, Self::Error> {
740        Self::new(value)
741    }
742}
743impl From<OwnerUserSid> for String {
744    #[inline]
745    fn from(value: OwnerUserSid) -> Self {
746        value.0.into()
747    }
748}
749
750/// Owner group SID (`fGSi`).
751#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
752#[repr(transparent)]
753pub struct OwnerGroupSid(BoundedString<OWNER_STR_MAX>);
754
755impl OwnerGroupSid {
756    /// Constructs an [`OwnerGroupSid`].
757    ///
758    /// # Errors
759    ///
760    /// Returns [`LengthExceeded`] when the byte length exceeds 255.
761    #[inline]
762    pub fn new(value: impl Into<Box<str>>) -> Result<Self, LengthExceeded> {
763        BoundedString::new(value).map(Self)
764    }
765    /// Returns the name as a string slice.
766    #[inline]
767    #[must_use]
768    pub fn as_str(&self) -> &str {
769        self.0.as_str()
770    }
771    pub(crate) fn to_bytes(&self) -> Vec<u8> {
772        let b = self.0.as_str().as_bytes();
773        let mut v = Vec::with_capacity(1 + b.len());
774        // Type guarantees b.len() <= 255 (BoundedString<255> invariant).
775        v.push(b.len() as u8);
776        v.extend_from_slice(b);
777        v
778    }
779    pub(crate) fn try_from_bytes(bytes: &[u8]) -> io::Result<Self> {
780        let (&len, rest) = bytes.split_first().ok_or(io::ErrorKind::UnexpectedEof)?;
781        let s = rest
782            .get(..len as usize)
783            .ok_or(io::ErrorKind::UnexpectedEof)?;
784        let s = str::from_utf8(s).map_err(|_| io::ErrorKind::InvalidData)?;
785        Self::new(s.to_owned()).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
786    }
787}
788
789impl Deref for OwnerGroupSid {
790    type Target = str;
791    #[inline]
792    fn deref(&self) -> &str {
793        self.0.as_str()
794    }
795}
796impl TryFrom<String> for OwnerGroupSid {
797    type Error = LengthExceeded;
798    #[inline]
799    fn try_from(value: String) -> Result<Self, Self::Error> {
800        Self::new(value)
801    }
802}
803impl TryFrom<&str> for OwnerGroupSid {
804    type Error = LengthExceeded;
805    #[inline]
806    fn try_from(value: &str) -> Result<Self, Self::Error> {
807        Self::new(value)
808    }
809}
810impl From<OwnerGroupSid> for String {
811    #[inline]
812    fn from(value: OwnerGroupSid) -> Self {
813        value.0.into()
814    }
815}
816
817/// Owner user id (`fUId`).
818#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
819#[repr(transparent)]
820pub struct OwnerUid(u64);
821
822impl OwnerUid {
823    /// Returns the raw user id.
824    #[inline]
825    #[must_use]
826    pub const fn get(self) -> u64 {
827        self.0
828    }
829    pub(crate) fn to_bytes(self) -> [u8; 8] {
830        self.0.to_be_bytes()
831    }
832    pub(crate) fn try_from_bytes(bytes: &[u8]) -> io::Result<Self> {
833        let a: [u8; 8] = bytes
834            .try_into()
835            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "fUId must be 8 bytes"))?;
836        Ok(Self(u64::from_be_bytes(a)))
837    }
838}
839impl From<u64> for OwnerUid {
840    #[inline]
841    fn from(v: u64) -> Self {
842        Self(v)
843    }
844}
845
846/// Owner group id (`fGId`).
847#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
848#[repr(transparent)]
849pub struct OwnerGid(u64);
850
851impl OwnerGid {
852    /// Returns the raw group id.
853    #[inline]
854    #[must_use]
855    pub const fn get(self) -> u64 {
856        self.0
857    }
858    pub(crate) fn to_bytes(self) -> [u8; 8] {
859        self.0.to_be_bytes()
860    }
861    pub(crate) fn try_from_bytes(bytes: &[u8]) -> io::Result<Self> {
862        let a: [u8; 8] = bytes
863            .try_into()
864            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "fGId must be 8 bytes"))?;
865        Ok(Self(u64::from_be_bytes(a)))
866    }
867}
868impl From<u64> for OwnerGid {
869    #[inline]
870    fn from(v: u64) -> Self {
871        Self(v)
872    }
873}
874
875/// POSIX permission mode (`fMOd`). Reserved bits outside `0o7777`
876/// (the rwx + setuid/setgid/sticky bits) are masked to 0 on construction.
877#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
878#[repr(transparent)]
879pub struct PermissionMode(u16);
880
881impl PermissionMode {
882    /// Returns the permission bits (`0o7777`-masked).
883    #[inline]
884    #[must_use]
885    pub const fn get(self) -> u16 {
886        self.0
887    }
888    pub(crate) fn to_bytes(self) -> [u8; 2] {
889        self.0.to_be_bytes()
890    }
891    pub(crate) fn try_from_bytes(bytes: &[u8]) -> io::Result<Self> {
892        let a: [u8; 2] = bytes
893            .try_into()
894            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "fMOd must be 2 bytes"))?;
895        Ok(Self::from(u16::from_be_bytes(a)))
896    }
897}
898impl From<u16> for PermissionMode {
899    #[inline]
900    fn from(v: u16) -> Self {
901        Self(v & 0o7777)
902    }
903}
904
905/// Link target type for link entries.
906///
907/// Stored in the `fLTP` ancillary chunk. Indicates whether the link target
908/// is a file or a directory. The semantic interpretation depends on the
909/// entry's [`DataKind`](crate::DataKind):
910///
911/// | `DataKind` | `Unknown` | `File` | `Directory` |
912/// |---|---|---|---|
913/// | `SymbolicLink` | Symlink (target unknown) | File symlink | Directory symlink |
914/// | `HardLink` | Hard link (target unknown) | File hard link | Directory hard link |
915///
916/// `HardLink` + `Directory` represents a directory hard link — a hard link
917/// whose target is a directory. On systems that prohibit hard links to
918/// directories, implementations may fall back to a symbolic link.
919///
920/// # Value assignments
921///
922/// - `Unknown` (0): Explicit unknown — the target type was not determined.
923/// - `File` (1): Target is a file.
924/// - `Directory` (2): Target is a directory.
925/// - Values 3–63 are reserved for future public extensions.
926/// - Values 64–255 are reserved for private extensions.
927/// - Both ranges are currently unrecognized and fall back to `None`.
928#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
929#[repr(u8)]
930pub enum LinkTargetType {
931    /// Link target type is unknown.
932    Unknown = 0,
933    /// Link target is a file.
934    File = 1,
935    /// Link target is a directory.
936    Directory = 2,
937}
938
939impl TryFrom<u8> for LinkTargetType {
940    type Error = UnknownValueError;
941
942    #[inline]
943    fn try_from(value: u8) -> Result<Self, Self::Error> {
944        match value {
945            0 => Ok(Self::Unknown),
946            1 => Ok(Self::File),
947            2 => Ok(Self::Directory),
948            value => Err(UnknownValueError(value)),
949        }
950    }
951}
952
953impl LinkTargetType {
954    pub(crate) fn to_bytes(self) -> [u8; 1] {
955        [self as u8]
956    }
957
958    /// Parse fLTP chunk data.
959    ///
960    /// - Known values (0, 1, 2): `Ok(Some(variant))`
961    /// - Unrecognized values (3-255): `Ok(None)` (graceful fallback)
962    /// - Insufficient data: `Err`
963    pub(crate) fn try_from_bytes(mut bytes: &[u8]) -> io::Result<Option<Self>> {
964        let mut buf = [0u8; 1];
965        bytes.read_exact(&mut buf)?;
966        Ok(Self::try_from(buf[0]).ok())
967    }
968}
969
970#[cfg(test)]
971mod tests {
972    use super::*;
973    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
974    use wasm_bindgen_test::wasm_bindgen_test as test;
975
976    #[allow(deprecated)]
977    #[test]
978    fn permission() {
979        let perm = Permission::new(1000, "user1".into(), 100, "group1".into(), 0o644);
980        assert_eq!(perm, Permission::try_from_bytes(&perm.to_bytes()).unwrap());
981    }
982
983    #[test]
984    fn owner_string_newtype_bound_and_codec() {
985        use crate::entry::OwnerUserName;
986        assert!(OwnerUserName::new("").is_ok());
987        assert!(OwnerUserName::new("alice").is_ok());
988        assert!(OwnerUserName::new("a".repeat(255)).is_ok());
989        assert!(OwnerUserName::new("a".repeat(256)).is_err());
990        let n = OwnerUserName::new("alice").unwrap();
991        assert_eq!(n.to_bytes(), vec![5, b'a', b'l', b'i', b'c', b'e']);
992        assert_eq!(OwnerUserName::try_from_bytes(&n.to_bytes()).unwrap(), n);
993        assert_eq!(OwnerUserName::try_from_bytes(&[0]).unwrap().as_str(), "");
994        assert_eq!(
995            OwnerUserName::try_from_bytes(&[3, b'a', b'b', b'c', 0xFF])
996                .unwrap()
997                .as_str(),
998            "abc"
999        );
1000        assert!(OwnerUserName::try_from_bytes(&[]).is_err());
1001        assert!(OwnerUserName::try_from_bytes(&[5, b'a']).is_err());
1002        assert!(OwnerUserName::try_from_bytes(&[1, 0xFF]).is_err());
1003    }
1004
1005    #[test]
1006    fn owner_uid_and_permission_mode_codec() {
1007        use crate::entry::{OwnerUid, PermissionMode};
1008        let u = OwnerUid::from(1000u64);
1009        assert_eq!(u.get(), 1000);
1010        assert_eq!(u.to_bytes(), 1000u64.to_be_bytes());
1011        assert_eq!(OwnerUid::try_from_bytes(&u.to_bytes()).unwrap(), u);
1012        assert!(OwnerUid::try_from_bytes(&[0, 0, 0]).is_err());
1013        assert_eq!(PermissionMode::from(0o7777u16).get(), 0o7777);
1014        assert_eq!(PermissionMode::from(0o170755u16).get(), 0o0755);
1015        let m = PermissionMode::from(0o644u16);
1016        assert_eq!(m.to_bytes(), 0o644u16.to_be_bytes());
1017        assert_eq!(PermissionMode::try_from_bytes(&m.to_bytes()).unwrap(), m);
1018        assert_eq!(
1019            PermissionMode::try_from_bytes(&0o170644u16.to_be_bytes())
1020                .unwrap()
1021                .get(),
1022            0o0644
1023        );
1024        assert!(PermissionMode::try_from_bytes(&[0]).is_err());
1025    }
1026
1027    #[test]
1028    fn metadata_owner_facets_default_none() {
1029        let m = Metadata::new();
1030        assert_eq!(m.owner_uid(), None);
1031        assert_eq!(m.owner_gid(), None);
1032        assert_eq!(m.owner_user_name(), None);
1033        assert_eq!(m.owner_group_name(), None);
1034        assert_eq!(m.owner_user_sid(), None);
1035        assert_eq!(m.owner_group_sid(), None);
1036        assert_eq!(m.permission_mode(), None);
1037    }
1038
1039    #[test]
1040    fn owner_id_facets_round_trip_via_entry() {
1041        use crate::entry::{OwnerGid, OwnerUid};
1042        use crate::{Archive, FileEntryBuilder};
1043        let mut buf = Vec::new();
1044        {
1045            let mut archive = Archive::write_header(&mut buf).unwrap();
1046            let mut b = FileEntryBuilder::new("f".into()).unwrap();
1047            b.metadata(
1048                Metadata::new()
1049                    .with_owner_uid(Some(OwnerUid::from(1000)))
1050                    .with_owner_gid(Some(OwnerGid::from(2000))),
1051            );
1052            let entry = b.build().unwrap();
1053            archive.add_entry(entry).unwrap();
1054            archive.finalize().unwrap();
1055        }
1056        let mut archive = Archive::read_header(&buf[..]).unwrap();
1057        let entry = archive.entries().skip_solid().next().unwrap().unwrap();
1058        let m = entry.metadata();
1059        assert_eq!(m.owner_uid().map(|v| v.get()), Some(1000));
1060        assert_eq!(m.owner_gid().map(|v| v.get()), Some(2000));
1061    }
1062
1063    #[test]
1064    fn owner_name_facets_round_trip_via_entry() {
1065        use crate::entry::{OwnerGroupName, OwnerUserName};
1066        use crate::{Archive, FileEntryBuilder};
1067        let mut buf = Vec::new();
1068        {
1069            let mut archive = Archive::write_header(&mut buf).unwrap();
1070            let mut b = FileEntryBuilder::new("f".into()).unwrap();
1071            b.metadata(
1072                Metadata::new()
1073                    .with_owner_user_name(Some(OwnerUserName::new("alice").unwrap()))
1074                    .with_owner_group_name(Some(OwnerGroupName::new("").unwrap())),
1075            );
1076            let entry = b.build().unwrap();
1077            archive.add_entry(entry).unwrap();
1078            archive.finalize().unwrap();
1079        }
1080        let mut archive = Archive::read_header(&buf[..]).unwrap();
1081        let entry = archive.entries().skip_solid().next().unwrap().unwrap();
1082        let m = entry.metadata();
1083        assert_eq!(m.owner_user_name().map(|v| v.as_str()), Some("alice"));
1084        assert_eq!(m.owner_group_name().map(|v| v.as_str()), Some("")); // recorded empty name, NOT absent
1085    }
1086
1087    #[test]
1088    fn owner_sid_facets_round_trip_via_entry() {
1089        use crate::entry::{OwnerGroupSid, OwnerUserSid};
1090        use crate::{Archive, FileEntryBuilder};
1091        let mut buf = Vec::new();
1092        {
1093            let mut archive = Archive::write_header(&mut buf).unwrap();
1094            let mut b = FileEntryBuilder::new("f".into()).unwrap();
1095            b.metadata(
1096                Metadata::new()
1097                    .with_owner_user_sid(Some(OwnerUserSid::new("S-1-5-21-1-2-3-1001").unwrap()))
1098                    .with_owner_group_sid(Some(OwnerGroupSid::new("S-1-5-32-544").unwrap())),
1099            );
1100            let entry = b.build().unwrap();
1101            archive.add_entry(entry).unwrap();
1102            archive.finalize().unwrap();
1103        }
1104        let mut archive = Archive::read_header(&buf[..]).unwrap();
1105        let entry = archive.entries().skip_solid().next().unwrap().unwrap();
1106        let m = entry.metadata();
1107        assert_eq!(
1108            m.owner_user_sid().map(|v| v.as_str()),
1109            Some("S-1-5-21-1-2-3-1001")
1110        );
1111        assert_eq!(
1112            m.owner_group_sid().map(|v| v.as_str()),
1113            Some("S-1-5-32-544")
1114        );
1115    }
1116
1117    #[test]
1118    fn fosi_length_prefixed_round_trip_and_empty() {
1119        use crate::entry::{OwnerGroupSid, OwnerUserSid};
1120        use crate::{Archive, FileEntryBuilder};
1121        let mut buf = Vec::new();
1122        {
1123            let mut a = Archive::write_header(&mut buf).unwrap();
1124            let mut b = FileEntryBuilder::new("f".into()).unwrap();
1125            b.metadata(
1126                Metadata::new()
1127                    .with_owner_user_sid(Some(OwnerUserSid::new("S-1-5-21-1-2-3-1001").unwrap()))
1128                    .with_owner_group_sid(Some(OwnerGroupSid::new("").unwrap())), // empty -> [0] -> Some("")
1129            );
1130            a.add_entry(b.build().unwrap()).unwrap();
1131            a.finalize().unwrap();
1132        }
1133        let mut a = Archive::read_header(&buf[..]).unwrap();
1134        let e = a.entries().skip_solid().next().unwrap().unwrap();
1135        assert_eq!(
1136            e.metadata().owner_user_sid().map(|v| v.as_str()),
1137            Some("S-1-5-21-1-2-3-1001")
1138        );
1139        assert_eq!(e.metadata().owner_group_sid().map(|v| v.as_str()), Some(""));
1140    }
1141
1142    #[test]
1143    fn permission_mode_facet_round_trip_via_entry() {
1144        use crate::entry::PermissionMode;
1145        use crate::{Archive, FileEntryBuilder};
1146        let mut buf = Vec::new();
1147        {
1148            let mut archive = Archive::write_header(&mut buf).unwrap();
1149            let mut b = FileEntryBuilder::new("f".into()).unwrap();
1150            b.metadata(Metadata::new().with_permission_mode(Some(PermissionMode::from(0o750))));
1151            let entry = b.build().unwrap();
1152            archive.add_entry(entry).unwrap();
1153            archive.finalize().unwrap();
1154        }
1155        let mut archive = Archive::read_header(&buf[..]).unwrap();
1156        let entry = archive.entries().skip_solid().next().unwrap().unwrap();
1157        assert_eq!(
1158            entry.metadata().permission_mode().map(|v| v.get()),
1159            Some(0o750)
1160        );
1161    }
1162
1163    #[test]
1164    fn link_target_type_roundtrip_unknown() {
1165        let ltp = LinkTargetType::Unknown;
1166        assert_eq!(
1167            Some(ltp),
1168            LinkTargetType::try_from_bytes(&ltp.to_bytes()).unwrap()
1169        );
1170    }
1171
1172    #[test]
1173    fn link_target_type_roundtrip_file() {
1174        let ltp = LinkTargetType::File;
1175        assert_eq!(
1176            Some(ltp),
1177            LinkTargetType::try_from_bytes(&ltp.to_bytes()).unwrap()
1178        );
1179    }
1180
1181    #[test]
1182    fn link_target_type_roundtrip_directory() {
1183        let ltp = LinkTargetType::Directory;
1184        assert_eq!(
1185            Some(ltp),
1186            LinkTargetType::try_from_bytes(&ltp.to_bytes()).unwrap()
1187        );
1188    }
1189
1190    #[test]
1191    fn link_target_type_unknown_values_return_none() {
1192        assert_eq!(LinkTargetType::try_from_bytes(&[0x03]).unwrap(), None);
1193        assert_eq!(LinkTargetType::try_from_bytes(&[0xFF]).unwrap(), None);
1194    }
1195
1196    #[test]
1197    fn link_target_type_empty_bytes() {
1198        assert!(LinkTargetType::try_from_bytes(&[]).is_err());
1199    }
1200
1201    #[test]
1202    fn link_target_type_try_from_u8() {
1203        assert_eq!(
1204            LinkTargetType::try_from(0u8).unwrap(),
1205            LinkTargetType::Unknown
1206        );
1207        assert_eq!(LinkTargetType::try_from(1u8).unwrap(), LinkTargetType::File);
1208        assert_eq!(
1209            LinkTargetType::try_from(2u8).unwrap(),
1210            LinkTargetType::Directory
1211        );
1212        assert!(LinkTargetType::try_from(3u8).is_err());
1213    }
1214
1215    #[test]
1216    fn link_target_type_trailing_bytes_ignored() {
1217        // read_exact reads only 1 byte; trailing bytes are silently ignored
1218        assert_eq!(
1219            LinkTargetType::try_from_bytes(&[0x01, 0xFF, 0xFF]).unwrap(),
1220            Some(LinkTargetType::File),
1221        );
1222    }
1223
1224    #[allow(deprecated)]
1225    #[test]
1226    fn all_owner_facets_and_fprm_coexist_round_trip() {
1227        use crate::entry::{
1228            OwnerGid, OwnerGroupName, OwnerGroupSid, OwnerUid, OwnerUserName, OwnerUserSid,
1229            PermissionMode,
1230        };
1231        use crate::{Archive, FileEntryBuilder};
1232        let mut buf = Vec::new();
1233        {
1234            let mut archive = Archive::write_header(&mut buf).unwrap();
1235            let mut b = FileEntryBuilder::new("f".into()).unwrap();
1236            b.metadata(
1237                Metadata::new()
1238                    // Legacy fPRM (Permission uses plain String uname/gname on this branch).
1239                    .with_permission(Some(Permission::new(
1240                        7,
1241                        "legacy".to_string(),
1242                        8,
1243                        "grp".to_string(),
1244                        0o600,
1245                    )))
1246                    // All 7 new owner facets.
1247                    .with_owner_uid(Some(OwnerUid::from(1)))
1248                    .with_owner_gid(Some(OwnerGid::from(2)))
1249                    .with_owner_user_name(Some(OwnerUserName::new("u").unwrap()))
1250                    .with_owner_group_name(Some(OwnerGroupName::new("g").unwrap()))
1251                    .with_owner_user_sid(Some(OwnerUserSid::new("S-1-1").unwrap()))
1252                    .with_owner_group_sid(Some(OwnerGroupSid::new("S-1-2").unwrap()))
1253                    .with_permission_mode(Some(PermissionMode::from(0o644))),
1254            );
1255            let entry = b.build().unwrap();
1256            archive.add_entry(entry).unwrap();
1257            archive.finalize().unwrap();
1258        }
1259        let mut archive = Archive::read_header(&buf[..]).unwrap();
1260        let entry = archive.entries().skip_solid().next().unwrap().unwrap();
1261        let m = entry.metadata();
1262        // All 7 new facets survived.
1263        assert_eq!(m.owner_uid().map(|v| v.get()), Some(1));
1264        assert_eq!(m.owner_gid().map(|v| v.get()), Some(2));
1265        assert_eq!(m.owner_user_name().map(|v| v.as_str()), Some("u"));
1266        assert_eq!(m.owner_group_name().map(|v| v.as_str()), Some("g"));
1267        assert_eq!(m.owner_user_sid().map(|v| v.as_str()), Some("S-1-1"));
1268        assert_eq!(m.owner_group_sid().map(|v| v.as_str()), Some("S-1-2"));
1269        assert_eq!(m.permission_mode().map(|v| v.get()), Some(0o644));
1270        // Legacy fPRM still round-trips intact, independently of the new owner facets.
1271        let p = m
1272            .permission()
1273            .expect("fPRM permission must still be present");
1274        assert_eq!(p.uid(), 7);
1275        assert_eq!(p.uname(), "legacy");
1276        assert_eq!(p.gid(), 8);
1277        assert_eq!(p.gname(), "grp");
1278        assert_eq!(p.permissions(), 0o600);
1279    }
1280
1281    #[test]
1282    fn fonm_trailing_bytes_after_length_are_ignored() {
1283        use crate::{Archive, ChunkType, FileEntryBuilder, RawChunk};
1284        let mut buf = Vec::new();
1285        {
1286            let mut a = Archive::write_header(&mut buf).unwrap();
1287            let mut b = FileEntryBuilder::new("g".into()).unwrap();
1288            b.add_extra_chunk(RawChunk::from_data(
1289                ChunkType::fONm,
1290                vec![3, b'a', b'b', b'c', 0xFF],
1291            ));
1292            a.add_entry(b.build().unwrap()).unwrap();
1293            a.finalize().unwrap();
1294        }
1295        let mut a = Archive::read_header(&buf[..]).unwrap();
1296        let e = a.entries().skip_solid().next().unwrap().unwrap();
1297        assert_eq!(
1298            e.metadata().owner_user_name().map(|v| v.as_str()),
1299            Some("abc")
1300        );
1301    }
1302}