Skip to main content

apfs_core/
dir.rs

1//! Directory records (`APFS_TYPE_DIR_REC 9`) and name→inode navigation.
2//!
3//! A directory entry's key is `j_drec_key_t` (parent oid + name) or, on
4//! case-insensitive/normalization-aware volumes, `j_drec_hashed_key_t` (parent
5//! oid + a packed name-length/hash word + name). After the 8-byte `j_key`
6//! header:
7//!
8//! - **unhashed** (`j_drec_key_t`): `name_len` u16 @8 (incl. the NUL), name @10.
9//! - **hashed** (`j_drec_hashed_key_t`): `name_len_and_hash` u32 @8
10//!   (`J_DREC_LEN_MASK 0x000003ff` = name length incl. NUL,
11//!   `J_DREC_HASH_SHIFT 10` for the 22-bit hash), name @12.
12//!
13//! The value `j_drec_val_t { u64 file_id; u64 date_added; u16 flags; xfields }`
14//! gives the target inode (`file_id`) and the time the entry was added
15//! (`date_added`). Layout verified verbatim against the Apple reference +
16//! libfsapfs format spec.
17//!
18//! **Navigation** resolves a path component by scanning the volume fs-tree for
19//! the `DIR_REC` whose key oid is the parent inode and whose name matches the
20//! requested component, returning the child inode (`file_id`); full-path
21//! resolution descends from `ROOT_DIR_INO_NUM` (2). The fs-tree is *virtual* —
22//! its node oids resolve through the volume object map at the volume's xid.
23
24use std::io::{Read, Seek};
25
26use crate::btree::{self, BTreeSubtype};
27use crate::fsrecord::{decode_jkey, RecordType};
28use crate::inode::Inode;
29use crate::object::{fletcher64_checksum, fletcher64_stored, ObjPhys};
30use crate::omap::ObjectMap;
31use crate::volume::ApfsVolume;
32
33/// `ROOT_DIR_INO_NUM` (Apple) — the inode number of a volume's root directory.
34pub const ROOT_DIR_INO_NUM: u64 = 2;
35
36/// `j_drec_hashed_key_t` name-length mask (low 10 bits of `name_len_and_hash`).
37const J_DREC_LEN_MASK: u32 = 0x0000_03ff;
38
39// j_drec_val_t value field offsets.
40const OFF_DREC_FILE_ID: usize = 0;
41const OFF_DREC_DATE_ADDED: usize = 8;
42const OFF_DREC_FLAGS: usize = 16;
43
44/// Depth cap on a path descent / fs-tree walk (cyclic-oid guard).
45const MAX_FSTREE_DEPTH: usize = 64;
46
47/// A directory entry.
48#[derive(Debug, Clone)]
49#[non_exhaustive]
50pub struct DirEntry {
51    /// The entry name (the directory-record key's name string).
52    pub name: String,
53    /// `file_id` — the target inode number.
54    pub file_id: u64,
55    /// `date_added` — when the entry was added (ns since 1970, distinct from the
56    /// inode's own timestamps; not updated on an in-place rename).
57    pub date_added: u64,
58    /// Directory-entry flags (`j_drec_val_t.flags`).
59    pub flags: u16,
60}
61
62/// Decode a `DIR_REC` key's name. The key begins with the 8-byte `j_key` header;
63/// the name layout depends on whether the volume uses hashed keys. The hashed
64/// form is detected structurally: its 4-byte `name_len_and_hash` low-10-bit
65/// length plus a name at offset 12 must fit the key; otherwise the unhashed
66/// (u16 length @8, name @10) form is used. Returns `None` if neither form's
67/// length fits the key slice (never panics, never over-reads).
68fn decode_drec_name(key: &[u8]) -> Option<String> {
69    // Hashed key: name_len_and_hash u32 @8, name @12.
70    let hashed_len = (crate::bytes::le_u32(key, 8) & J_DREC_LEN_MASK) as usize;
71    if hashed_len > 0 {
72        if let Some(name) = key.get(12..12 + hashed_len) {
73            return Some(decode_cstr(name));
74        }
75    }
76    // Unhashed key: name_len u16 @8, name @10.
77    let unhashed_len = crate::bytes::le_u16(key, 8) as usize;
78    if unhashed_len > 0 {
79        if let Some(name) = key.get(10..10 + unhashed_len) {
80            return Some(decode_cstr(name));
81        }
82    }
83    None
84}
85
86/// Parse a `DIR_REC` (key, value) pair into a [`DirEntry`]. `None` if the name
87/// cannot be decoded from the key (a malformed record is skipped, not fatal).
88fn parse_dir_entry(key: &[u8], value: &[u8]) -> Option<DirEntry> {
89    let name = decode_drec_name(key)?;
90    Some(DirEntry {
91        name,
92        file_id: crate::bytes::le_u64(value, OFF_DREC_FILE_ID),
93        date_added: crate::bytes::le_u64(value, OFF_DREC_DATE_ADDED),
94        flags: crate::bytes::le_u16(value, OFF_DREC_FLAGS),
95    })
96}
97
98/// Walk the volume fs-tree (a *virtual* B-tree resolved through the volume omap)
99/// keyed to a single object id: visit only the records whose `j_key` object id is
100/// `target_oid`, descending one root→leaf path per node level instead of the
101/// whole tree. The fs-tree is sorted by object id first, so all records for one
102/// object id occupy a contiguous key range; at each index node only the children
103/// whose key range can cover `target_oid` are descended (see
104/// [`child_may_contain_oid`]). Each node's Fletcher-64 checksum is verified
105/// before its TOC is trusted, the descent depth is capped, and a visited-set
106/// guards against cyclic node oids. The `visit` callback still sees the landing
107/// leaves' entries and must filter precisely (a leaf may also hold neighbours).
108///
109/// `pub(crate)` so every navigation entry point shares it — `lookup_child`,
110/// `load_inode`, `list_dir`, [`crate::extent::list_extents`],
111/// [`crate::xattr::list_xattrs`] — since each filters by a single object id,
112/// without duplicating the omap-resolution + checksum/cycle-guard walk.
113///
114/// # Errors
115/// [`crate::ApfsError::OmapUnresolved`] / [`crate::ApfsError::ChecksumMismatch`]
116/// / [`crate::ApfsError::CycleGuard`] / [`crate::ApfsError::Io`] on a
117/// structurally invalid omap/fs-tree or a read failure.
118pub(crate) fn for_each_fs_record_for_oid<R, F>(
119    reader: &mut R,
120    volume: &ApfsVolume,
121    block_size: usize,
122    target_oid: u64,
123    visit: &mut F,
124) -> crate::Result<()>
125where
126    R: Read + Seek,
127    F: FnMut(&[u8], &[u8]),
128{
129    walk_fs_tree(reader, volume, block_size, Some(target_oid), visit)
130}
131
132fn walk_fs_tree<R, F>(
133    reader: &mut R,
134    volume: &ApfsVolume,
135    block_size: usize,
136    target_oid: Option<u64>,
137    visit: &mut F,
138) -> crate::Result<()>
139where
140    R: Read + Seek,
141    F: FnMut(&[u8], &[u8]),
142{
143    // Read the volume omap header (a physical object at apfs_omap_oid).
144    let mut buf = vec![0u8; block_size];
145    let omap_off = volume.omap_oid().saturating_mul(block_size as u64);
146    reader.seek(std::io::SeekFrom::Start(omap_off))?;
147    reader.read_exact(&mut buf)?;
148    let omap = ObjectMap::parse(&buf)?;
149
150    let xid = volume.xid();
151    let mut visited = std::collections::HashSet::new();
152    descend_virtual(
153        reader,
154        &omap,
155        volume.root_tree_oid(),
156        xid,
157        block_size,
158        0,
159        target_oid,
160        &mut visited,
161        visit,
162    )
163}
164
165/// Whether an index-node child whose subtree covers keys `[sep, next_sep)` can
166/// contain a record with object id `target`. `next_sep` is the next separator's
167/// object id, or `None` for the last child (its subtree extends upward without
168/// bound). Records for one object id form a contiguous key range, so the child
169/// is relevant iff its low bound is ≤ `target` and its high bound is ≥ `target`.
170///
171/// The `next_sep == target` boundary **must** descend: a separator is the first
172/// *full* key of the next child, so a record `(target, low_type)` smaller than
173/// that separator can still live at the end of *this* child.
174fn child_may_contain_oid(sep_oid: u64, next_sep_oid: Option<u64>, target: u64) -> bool {
175    sep_oid <= target && next_sep_oid.is_none_or(|next| next >= target)
176}
177
178#[allow(clippy::too_many_arguments)]
179fn descend_virtual<R, F>(
180    reader: &mut R,
181    omap: &ObjectMap,
182    node_oid: u64,
183    xid: u64,
184    block_size: usize,
185    depth: usize,
186    target_oid: Option<u64>,
187    visited: &mut std::collections::HashSet<u64>,
188    visit: &mut F,
189) -> crate::Result<()>
190where
191    R: Read + Seek,
192    F: FnMut(&[u8], &[u8]),
193{
194    let cycle = || crate::ApfsError::CycleGuard {
195        cap: MAX_FSTREE_DEPTH,
196    };
197    // The visited-set guard below dominates — any cycle repeats a node oid
198    // (tripping it) before a legal tree reaches depth 64; this depth cap is
199    // defense-in-depth against a pathological deep acyclic tree.
200    if depth >= MAX_FSTREE_DEPTH {
201        return Err(cycle()); // cov:unreachable: visited-set guard dominates any realizable cycle
202    }
203    if !visited.insert(node_oid) {
204        return Err(cycle());
205    }
206
207    // Resolve this node's virtual oid to a physical block via the omap.
208    let entry = omap.resolve(reader, node_oid, xid, block_size)?;
209
210    let mut buf = vec![0u8; block_size];
211    let offset = entry.paddr.saturating_mul(block_size as u64);
212    reader.seek(std::io::SeekFrom::Start(offset))?;
213    reader.read_exact(&mut buf)?;
214
215    // Checksum-before-trust.
216    let stored = fletcher64_stored(&buf);
217    let computed = fletcher64_checksum(&buf);
218    if stored != computed {
219        let block = ObjPhys::parse(&buf).map_or(entry.paddr, |h| h.oid);
220        return Err(crate::ApfsError::ChecksumMismatch {
221            block,
222            stored,
223            computed,
224        });
225    }
226
227    let Some(hdr) = btree::parse_node_header(&buf) else {
228        return Ok(()); // cov:unreachable: buf is block_size >= node header length
229    };
230
231    if hdr.is_leaf() {
232        for e in btree::node_entries(&buf, BTreeSubtype::FsTree) {
233            visit(e.key, e.value);
234        }
235        return Ok(());
236    }
237
238    // Index node: each value is an 8-byte child *virtual* oid. For a keyed walk,
239    // descend only the children whose key range can cover `target_oid`; a full
240    // walk (target_oid == None) descends every child.
241    let entries = btree::node_entries(&buf, BTreeSubtype::FsTree);
242    for i in 0..entries.len() {
243        if let Some(target) = target_oid {
244            let (sep_oid, _) = decode_jkey(crate::bytes::le_u64(entries[i].key, 0));
245            let next_sep_oid = entries
246                .get(i + 1)
247                .map(|e| decode_jkey(crate::bytes::le_u64(e.key, 0)).0);
248            if !child_may_contain_oid(sep_oid, next_sep_oid, target) {
249                continue;
250            }
251        }
252        let child = crate::bytes::le_u64(entries[i].value, 0);
253        descend_virtual(
254            reader,
255            omap,
256            child,
257            xid,
258            block_size,
259            depth + 1,
260            target_oid,
261            visited,
262            visit,
263        )?;
264    }
265    Ok(())
266}
267
268/// List the directory entries whose parent is `parent_oid`, scanning the volume
269/// fs-tree for `DIR_REC` records.
270///
271/// # Errors
272/// [`crate::ApfsError::OmapUnresolved`] / [`crate::ApfsError::ChecksumMismatch`]
273/// / [`crate::ApfsError::CycleGuard`] / [`crate::ApfsError::Io`] on a
274/// structurally invalid omap/fs-tree or a read failure.
275pub fn list_dir<R: Read + Seek>(
276    reader: &mut R,
277    volume: &ApfsVolume,
278    parent_oid: u64,
279    block_size: usize,
280) -> crate::Result<Vec<DirEntry>> {
281    let mut out = Vec::new();
282    for_each_fs_record_for_oid(reader, volume, block_size, parent_oid, &mut |key, value| {
283        let (oid, ty) = decode_jkey(crate::bytes::le_u64(key, 0));
284        if ty != Some(RecordType::DirRec) || oid != parent_oid {
285            return;
286        }
287        // Skip a malformed DIR_REC rather than fail the whole listing.
288        let Some(entry) = parse_dir_entry(key, value) else {
289            return; // cov:unreachable: valid DIR_REC keys always decode a name
290        };
291        out.push(entry);
292    })?;
293    Ok(out)
294}
295
296/// Resolve a single path component: look up the `DIR_REC` `(parent_oid, name)` in
297/// the fs-tree and return the child inode number, or `None` if absent.
298///
299/// # Errors
300/// As [`list_dir`].
301pub fn lookup_child<R: Read + Seek>(
302    reader: &mut R,
303    volume: &ApfsVolume,
304    parent_oid: u64,
305    name: &str,
306    block_size: usize,
307) -> crate::Result<Option<u64>> {
308    let mut found = None;
309    for_each_fs_record_for_oid(reader, volume, block_size, parent_oid, &mut |key, value| {
310        if found.is_some() {
311            return;
312        }
313        let (oid, ty) = decode_jkey(crate::bytes::le_u64(key, 0));
314        if ty != Some(RecordType::DirRec) || oid != parent_oid {
315            return;
316        }
317        // A DIR_REC whose name cannot be decoded is a malformed/hostile record;
318        // skip it rather than fail the whole listing (defensive — real images
319        // always decode, so the skip arm is unreachable on valid data).
320        let Some(entry) = parse_dir_entry(key, value) else {
321            return; // cov:unreachable: valid DIR_REC keys always decode a name
322        };
323        if entry.name == name {
324            found = Some(entry.file_id);
325        }
326    })?;
327    Ok(found)
328}
329
330/// Load the inode (`INODE` record) for `oid` from the volume fs-tree.
331///
332/// # Errors
333/// [`crate::ApfsError::OmapUnresolved`] if the inode record is not present (a
334/// loud per-item miss), plus the structural errors of [`list_dir`].
335pub fn load_inode<R: Read + Seek>(
336    reader: &mut R,
337    volume: &ApfsVolume,
338    oid: u64,
339    block_size: usize,
340) -> crate::Result<Inode> {
341    let mut value: Option<Vec<u8>> = None;
342    for_each_fs_record_for_oid(reader, volume, block_size, oid, &mut |key, val| {
343        if value.is_some() {
344            return;
345        }
346        let (k_oid, ty) = decode_jkey(crate::bytes::le_u64(key, 0));
347        if ty == Some(RecordType::Inode) && k_oid == oid {
348            value = Some(val.to_vec());
349        }
350    })?;
351    let value = value.ok_or(crate::ApfsError::OmapUnresolved {
352        oid,
353        xid: volume.xid(),
354    })?;
355    Inode::parse(oid, &value)
356}
357
358/// Resolve a `/`-separated path to an [`Inode`] by descending the fs-tree from
359/// the root directory (`ROOT_DIR_INO_NUM`). Empty components (from a leading,
360/// trailing, or doubled `/`) are skipped; `"/"` resolves to the root inode.
361///
362/// # Errors
363/// [`crate::ApfsError::OmapUnresolved`] if any path component is not found or the
364/// final inode record is absent (a loud per-item miss), plus the structural
365/// errors of [`list_dir`].
366pub fn open_path<R: Read + Seek>(
367    reader: &mut R,
368    volume: &ApfsVolume,
369    path: &str,
370    block_size: usize,
371) -> crate::Result<Inode> {
372    let mut current = ROOT_DIR_INO_NUM;
373    for component in path.split('/').filter(|c| !c.is_empty()) {
374        match lookup_child(reader, volume, current, component, block_size)? {
375            Some(child) => current = child,
376            None => {
377                return Err(crate::ApfsError::OmapUnresolved {
378                    oid: current,
379                    xid: volume.xid(),
380                });
381            }
382        }
383    }
384    load_inode(reader, volume, current, block_size)
385}
386
387/// Decode a NUL-terminated UTF-8 byte string. Bytes after the first NUL are
388/// dropped; invalid UTF-8 is replaced (never panics).
389fn decode_cstr(data: &[u8]) -> String {
390    let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
391    String::from_utf8_lossy(&data[..end]).into_owned()
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    /// Build a `j_key` header word from a 4-bit type and a 60-bit oid.
399    fn jkey(ty: u64, oid: u64) -> [u8; 8] {
400        ((ty << 60) | oid).to_le_bytes()
401    }
402
403    #[test]
404    fn decode_hashed_drec_name() {
405        // 8-byte j_key header (DIR_REC, parent 2), then name_len_and_hash u32 with
406        // low-10-bit length = 5 ("abcd\0"), then the name at offset 12.
407        let mut key = Vec::new();
408        key.extend_from_slice(&jkey(9, 2));
409        key.extend_from_slice(&5u32.to_le_bytes()); // len=5 in low 10 bits, hash 0
410        key.extend_from_slice(b"abcd\0");
411        assert_eq!(decode_drec_name(&key).as_deref(), Some("abcd"));
412    }
413
414    #[test]
415    fn child_pruning_selects_only_covering_subtrees() {
416        // Index node with separators [oid 0, oid 10, oid 20]; child i covers
417        // [sep_i, sep_{i+1}). Records for one oid form a contiguous key range.
418        // target oid 15 lives only under child[1] (covers [10, 20)).
419        assert!(
420            !child_may_contain_oid(0, Some(10), 15),
421            "child [0,10) excludes 15"
422        );
423        assert!(
424            child_may_contain_oid(10, Some(20), 15),
425            "child [10,20) covers 15"
426        );
427        assert!(
428            !child_may_contain_oid(20, None, 15),
429            "child [20,inf) excludes 15"
430        );
431
432        // Boundary spill: a separator is the next child's FIRST FULL key, so a
433        // record (target, low_type) below it can sit at the END of THIS child.
434        // Hence next_sep == target MUST still descend.
435        assert!(
436            child_may_contain_oid(5, Some(10), 10),
437            "next_sep == target must descend (record may trail in this child)"
438        );
439        // sep == target obviously descends; last child always covers the high side.
440        assert!(child_may_contain_oid(10, Some(20), 10));
441        assert!(child_may_contain_oid(5, None, 999));
442        // Entirely below the target prunes.
443        assert!(!child_may_contain_oid(0, Some(5), 10));
444    }
445
446    // Real-data cross-check: on the Apple-authored fs-tree, the KEYED walk must
447    // visit exactly the records the FULL (unpruned) walk visits when filtered to
448    // the same object id. The full walk — the same descent with no pruning, whose
449    // results are independently validated against `fls`/`istat` by the dir/inode
450    // integration tests — is the oracle, so this proves the keyed pruning never
451    // drops a covering record.
452    const FSTREE: &[u8] = include_bytes!("../../tests/data/apfs_fstree.bin");
453    const FSTREE_BLOCK_SIZE: usize = 4096;
454    const FSTREE_APSB_BLOCK: usize = 371;
455
456    fn fstree_volume() -> ApfsVolume {
457        let b = &FSTREE
458            [FSTREE_APSB_BLOCK * FSTREE_BLOCK_SIZE..(FSTREE_APSB_BLOCK + 1) * FSTREE_BLOCK_SIZE];
459        ApfsVolume::parse(b).expect("parse APSB")
460    }
461
462    fn keys_for_oid_full(oid: u64) -> Vec<Vec<u8>> {
463        use std::io::Cursor;
464        let mut r = Cursor::new(FSTREE);
465        let vol = fstree_volume();
466        let mut out = Vec::new();
467        // walk_fs_tree(None) is the full, unpruned descent — the oracle.
468        walk_fs_tree(&mut r, &vol, FSTREE_BLOCK_SIZE, None, &mut |k, _| {
469            if decode_jkey(crate::bytes::le_u64(k, 0)).0 == oid {
470                out.push(k.to_vec());
471            }
472        })
473        .expect("full walk");
474        out
475    }
476
477    fn keys_for_oid_keyed(oid: u64) -> Vec<Vec<u8>> {
478        use std::io::Cursor;
479        let mut r = Cursor::new(FSTREE);
480        let vol = fstree_volume();
481        let mut out = Vec::new();
482        for_each_fs_record_for_oid(&mut r, &vol, FSTREE_BLOCK_SIZE, oid, &mut |k, _| {
483            if decode_jkey(crate::bytes::le_u64(k, 0)).0 == oid {
484                out.push(k.to_vec());
485            }
486        })
487        .expect("keyed walk");
488        out
489    }
490
491    #[test]
492    fn keyed_walk_matches_full_walk_on_real_fs_tree() {
493        // Root dir (2), Dir1 (18), and a file inode (22) — each must yield the
494        // same record set keyed as it does via the filtered full walk.
495        for oid in [2u64, 18, 22] {
496            assert_eq!(
497                keys_for_oid_keyed(oid),
498                keys_for_oid_full(oid),
499                "keyed vs full record set for oid {oid}"
500            );
501        }
502        // A non-existent oid yields nothing either way.
503        assert!(keys_for_oid_keyed(999_999).is_empty());
504        assert_eq!(keys_for_oid_keyed(999_999), keys_for_oid_full(999_999));
505    }
506
507    #[test]
508    fn decode_unhashed_drec_name() {
509        // A case-sensitive volume uses j_drec_key_t: name_len u16 @8, name @10.
510        // Force the hashed path to miss (its low-10-bit length must not fit) by
511        // making the u32 length point past the key, then the unhashed fallback
512        // decodes "Xy\0" (len 3) at offset 10.
513        let mut key = Vec::new();
514        key.extend_from_slice(&jkey(9, 2));
515        key.extend_from_slice(&3u16.to_le_bytes()); // name_len = 3
516        key.extend_from_slice(b"Xy\0");
517        // The hashed interpretation reads name_len_and_hash = u32 @8. Here the
518        // upper two name bytes ("Xy") become part of that u32, giving a bogus
519        // hashed length that runs past the key, so the unhashed branch is used.
520        let name = decode_drec_name(&key);
521        assert_eq!(name.as_deref(), Some("Xy"));
522    }
523
524    #[test]
525    fn decode_drec_name_rejects_overlong_length() {
526        // A key claiming a name longer than its bytes yields None (no over-read).
527        let mut key = Vec::new();
528        key.extend_from_slice(&jkey(9, 2));
529        key.extend_from_slice(&0u32.to_le_bytes()); // hashed len 0
530                                                    // no name bytes; unhashed len also reads 0 -> None
531        assert_eq!(decode_drec_name(&key), None);
532    }
533
534    #[test]
535    fn decode_drec_name_unhashed_length_past_key_is_none() {
536        // unhashed_len > 0 but the name slice runs past the key, AND the hashed
537        // interpretation's length also doesn't fit: both branches miss -> None
538        // (exercises the unhashed `key.get(..)` None arm — no over-read).
539        let mut key = Vec::new();
540        key.extend_from_slice(&jkey(9, 2));
541        // name_len u16 @8 = 200 (way past the key); the hashed u32 @8 low-10-bit
542        // length is also 200, whose name@12 slice does not fit either.
543        key.extend_from_slice(&200u16.to_le_bytes());
544        key.extend_from_slice(b"z"); // only one trailing byte
545        assert_eq!(decode_drec_name(&key), None);
546    }
547
548    #[test]
549    fn parse_dir_entry_decodes_value() {
550        let mut key = Vec::new();
551        key.extend_from_slice(&jkey(9, 2));
552        key.extend_from_slice(&5u32.to_le_bytes());
553        key.extend_from_slice(b"abcd\0");
554        let mut value = Vec::new();
555        value.extend_from_slice(&42u64.to_le_bytes()); // file_id
556        value.extend_from_slice(&1234u64.to_le_bytes()); // date_added
557        value.extend_from_slice(&7u16.to_le_bytes()); // flags
558        let e = parse_dir_entry(&key, &value).expect("parse drec");
559        assert_eq!(e.name, "abcd");
560        assert_eq!(e.file_id, 42);
561        assert_eq!(e.date_added, 1234);
562        assert_eq!(e.flags, 7);
563    }
564
565    #[test]
566    fn parse_dir_entry_rejects_unnamed_key() {
567        // A key with no decodable name yields None (the record is skipped).
568        let mut key = Vec::new();
569        key.extend_from_slice(&jkey(9, 2));
570        key.extend_from_slice(&0u32.to_le_bytes());
571        assert!(parse_dir_entry(&key, &[0u8; 18]).is_none());
572    }
573}