1use crate::block_io::BlockDevice;
30use crate::error::{Error, Result};
31use crate::fs::Filesystem;
32use crate::inode::Inode;
33
34pub const EXT4_XATTR_MAGIC: u32 = 0xEA02_0000;
36
37pub const NAME_PREFIXES: &[(u8, &str)] = &[
39 (1, "user."),
40 (2, "system.posix_acl_access"),
41 (3, "system.posix_acl_default"),
42 (4, "trusted."),
43 (5, "lustre."),
44 (6, "security."),
45 (7, "system."),
46 (8, "system.richacl"),
47];
48
49pub fn prefix_for_index(idx: u8) -> Option<&'static str> {
51 NAME_PREFIXES
52 .iter()
53 .find(|(i, _)| *i == idx)
54 .map(|(_, s)| *s)
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
68#[non_exhaustive]
69pub struct XattrEntry {
70 pub name: String,
72 pub value: Vec<u8>,
77 pub value_inum: u32,
86 pub value_size: u32,
96}
97
98impl XattrEntry {
99 pub fn new(name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
105 let value = value.into();
106 let value_size = u32::try_from(value.len()).expect("an xattr value fits in u32");
107 Self {
108 name: name.into(),
109 value,
110 value_inum: 0,
111 value_size,
112 }
113 }
114
115 pub fn in_ea_inode(name: impl Into<String>, value_inum: u32, value_size: u32) -> Self {
119 Self {
120 name: name.into(),
121 value: Vec::new(),
122 value_inum,
123 value_size,
124 }
125 }
126}
127
128pub fn read_all(
137 dev: &dyn BlockDevice,
138 inode: &Inode,
139 inode_raw: &[u8],
140 inode_size: u16,
141 block_size: u32,
142) -> Result<Vec<XattrEntry>> {
143 let mut out = Vec::new();
144
145 if inode_raw.len() >= 128 + 4 {
147 let extra_isize = u16::from_le_bytes(inode_raw[128..130].try_into().unwrap()) as usize;
148 let xattr_region_start = 128 + extra_isize;
149 if extra_isize != 0
156 && xattr_region_start + 4 <= inode_size as usize
157 && xattr_region_start + 4 <= inode_raw.len()
158 {
159 let region = &inode_raw[xattr_region_start..(inode_size as usize).min(inode_raw.len())];
160 let magic = u32::from_le_bytes(region[..4].try_into().unwrap());
161 if magic == EXT4_XATTR_MAGIC {
162 parse_entries(®ion[4..], region.len() - 4, &mut out)?;
165 }
166 }
167 }
168
169 if inode.file_acl != 0 {
171 let mut buf = vec![0u8; block_size as usize];
172 dev.read_at(inode.file_acl * block_size as u64, &mut buf)?;
173 let magic = u32::from_le_bytes(buf[..4].try_into().unwrap());
174 if magic != EXT4_XATTR_MAGIC {
175 return Err(Error::Corrupt("xattr block magic mismatch"));
176 }
177 parse_entries_block(&buf, &mut out)?;
180 }
181
182 Ok(out)
183}
184
185fn parse_entries(entries_buf: &[u8], _region_len: usize, out: &mut Vec<XattrEntry>) -> Result<()> {
192 let mut pos = 0;
193 while pos + 16 <= entries_buf.len() {
194 let header = u32::from_le_bytes(entries_buf[pos..pos + 4].try_into().unwrap());
202 if header == 0 {
203 break;
204 }
205 let name_len = entries_buf[pos] as usize;
206 let name_index = entries_buf[pos + 1];
207 let value_offs =
208 u16::from_le_bytes(entries_buf[pos + 2..pos + 4].try_into().unwrap()) as usize;
209 let value_inum = u32::from_le_bytes(entries_buf[pos + 4..pos + 8].try_into().unwrap());
210 let value_size =
211 u32::from_le_bytes(entries_buf[pos + 8..pos + 12].try_into().unwrap()) as usize;
212 let entry_size = 16 + name_len;
215 let entry_padded = (entry_size + 3) & !3;
216 if pos + 16 + name_len > entries_buf.len() {
217 return Err(Error::Corrupt("xattr entry name overruns region"));
218 }
219
220 let name_bytes = &entries_buf[pos + 16..pos + 16 + name_len];
221 let prefix = prefix_for_index(name_index).unwrap_or("");
222 let suffix =
223 std::str::from_utf8(name_bytes).map_err(|_| Error::Corrupt("xattr name not utf-8"))?;
224 let full_name = format!("{prefix}{suffix}");
225
226 let value = if value_inum != 0 {
232 Vec::new()
233 } else if value_size > 0 {
234 if value_offs + value_size > entries_buf.len() {
235 return Err(Error::Corrupt("xattr value out of range"));
236 }
237 entries_buf[value_offs..value_offs + value_size].to_vec()
238 } else {
239 Vec::new()
240 };
241
242 out.push(XattrEntry {
243 name: full_name,
244 value,
245 value_inum,
246 value_size: value_size as u32,
247 });
248
249 pos += entry_padded;
250 }
251 Ok(())
252}
253
254fn parse_entries_block(block: &[u8], out: &mut Vec<XattrEntry>) -> Result<()> {
258 if block.len() < 32 {
259 return Err(Error::Corrupt("xattr block too small"));
260 }
261
262 let mut pos = 32; while pos + 16 <= block.len() {
264 let header = u32::from_le_bytes(block[pos..pos + 4].try_into().unwrap());
267 if header == 0 {
268 break;
269 }
270 let name_len = block[pos] as usize;
271 let name_index = block[pos + 1];
272 let value_offs = u16::from_le_bytes(block[pos + 2..pos + 4].try_into().unwrap()) as usize;
273 let value_inum = u32::from_le_bytes(block[pos + 4..pos + 8].try_into().unwrap());
274 let value_size = u32::from_le_bytes(block[pos + 8..pos + 12].try_into().unwrap()) as usize;
275
276 let entry_size = 16 + name_len;
277 let entry_padded = (entry_size + 3) & !3;
278 if pos + 16 + name_len > block.len() {
279 return Err(Error::Corrupt("xattr entry name overruns block"));
280 }
281
282 let name_bytes = &block[pos + 16..pos + 16 + name_len];
283 let prefix = prefix_for_index(name_index).unwrap_or("");
284 let suffix =
285 std::str::from_utf8(name_bytes).map_err(|_| Error::Corrupt("xattr name not utf-8"))?;
286 let full_name = format!("{prefix}{suffix}");
287
288 let value = if value_inum != 0 {
291 Vec::new()
292 } else if value_size > 0 {
293 if value_offs + value_size > block.len() {
294 return Err(Error::Corrupt("xattr block value out of range"));
295 }
296 block[value_offs..value_offs + value_size].to_vec()
297 } else {
298 Vec::new()
299 };
300
301 out.push(XattrEntry {
302 name: full_name,
303 value,
304 value_inum,
305 value_size: value_size as u32,
306 });
307
308 pos += entry_padded;
309 }
310 Ok(())
311}
312
313pub fn split_qualified_name(name: &str) -> Option<(u8, &str)> {
316 for (idx, prefix) in NAME_PREFIXES {
317 if let Some(rest) = name.strip_prefix(*prefix) {
318 return Some((*idx, rest));
319 }
320 }
321 None
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
327pub enum RemoveOutcome {
328 Removed,
330 NotFound,
332}
333
334pub fn plan_remove_in_inode_region(region: &mut [u8], name: &str) -> Result<RemoveOutcome> {
341 let Some((name_index, suffix)) = split_qualified_name(name) else {
342 return Err(Error::InvalidArgument(
343 "xattr name missing known namespace prefix",
344 ));
345 };
346 if region.len() < 4 {
347 return Ok(RemoveOutcome::NotFound);
348 }
349 let magic = u32::from_le_bytes(region[..4].try_into().unwrap());
350 if magic != EXT4_XATTR_MAGIC {
351 return Ok(RemoveOutcome::NotFound);
352 }
353
354 let entries = decode_in_inode_entries(®ion[4..])?;
356 refuse_if_any_ea_inode_backed(&entries)?;
357 let before = entries.len();
358 let kept: Vec<DecodedEntry> = entries
359 .into_iter()
360 .filter(|e| !(e.name_index == name_index && e.name_bytes == suffix.as_bytes()))
361 .collect();
362 if kept.len() == before {
363 return Ok(RemoveOutcome::NotFound);
364 }
365
366 encode_in_inode_entries(region, &kept);
367 Ok(RemoveOutcome::Removed)
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
374pub enum SetOutcome {
375 Inserted,
376 Replaced,
377}
378
379pub fn plan_set_in_inode_region(region: &mut [u8], name: &str, value: &[u8]) -> Result<SetOutcome> {
390 let Some((name_index, suffix)) = split_qualified_name(name) else {
391 return Err(Error::InvalidArgument(
392 "xattr name missing known namespace prefix",
393 ));
394 };
395 if suffix.is_empty() && !matches!(name_index, 2 | 3) {
396 return Err(Error::InvalidArgument("xattr name suffix is empty"));
397 }
398 if suffix.len() > 255 {
399 return Err(Error::NameTooLong);
400 }
401 if region.len() < 8 {
402 return Err(Error::NoSpaceLeftOnDevice);
403 }
404
405 let magic_present = {
406 let m = u32::from_le_bytes(region[..4].try_into().unwrap());
407 m == EXT4_XATTR_MAGIC
408 };
409 let mut entries = if magic_present {
410 decode_in_inode_entries(®ion[4..])?
411 } else {
412 Vec::new()
413 };
414 refuse_if_any_ea_inode_backed(&entries)?;
415
416 let mut outcome = SetOutcome::Inserted;
417 let suffix_bytes = suffix.as_bytes();
418 for e in entries.iter_mut() {
419 if e.name_index == name_index && e.name_bytes == suffix_bytes {
420 e.value = value.to_vec();
421 outcome = SetOutcome::Replaced;
422 break;
423 }
424 }
425 if matches!(outcome, SetOutcome::Inserted) {
426 entries.push(DecodedEntry {
427 name_index,
428 name_bytes: suffix_bytes.to_vec(),
429 value: value.to_vec(),
430 value_inum: 0,
431 });
432 }
433
434 let area_len = region.len() - 4;
435 let needed_entries: usize = entries
436 .iter()
437 .map(|e| (16 + e.name_bytes.len() + 3) & !3)
438 .sum();
439 let needed_values: usize = entries
440 .iter()
441 .filter(|e| !e.value.is_empty())
442 .map(|e| (e.value.len() + 3) & !3)
443 .sum();
444 if needed_entries + 4 + needed_values > area_len {
445 return Err(Error::NoSpaceLeftOnDevice);
446 }
447
448 encode_in_inode_entries(region, &entries);
449 Ok(outcome)
450}
451
452#[derive(Debug, Clone)]
454struct DecodedEntry {
455 name_index: u8,
456 name_bytes: Vec<u8>,
457 value: Vec<u8>,
458 value_inum: u32,
465}
466
467fn refuse_if_any_ea_inode_backed(entries: &[DecodedEntry]) -> Result<()> {
484 if entries.iter().any(|e| e.value_inum != 0) {
485 return Err(Error::Unsupported(
486 "this inode has an extended attribute whose value lives in an EA inode; \
487 rewriting the attribute area would orphan it",
488 ));
489 }
490 Ok(())
491}
492
493fn decode_in_inode_entries(entries_buf: &[u8]) -> Result<Vec<DecodedEntry>> {
496 let mut out = Vec::new();
497 let mut pos = 0usize;
498 while pos + 16 <= entries_buf.len() {
499 let header = u32::from_le_bytes(entries_buf[pos..pos + 4].try_into().unwrap());
500 if header == 0 {
501 break;
502 }
503 let name_len = entries_buf[pos] as usize;
504 let name_index = entries_buf[pos + 1];
505 let value_offs =
506 u16::from_le_bytes(entries_buf[pos + 2..pos + 4].try_into().unwrap()) as usize;
507 let value_inum = u32::from_le_bytes(entries_buf[pos + 4..pos + 8].try_into().unwrap());
508 let value_size =
509 u32::from_le_bytes(entries_buf[pos + 8..pos + 12].try_into().unwrap()) as usize;
510 if pos + 16 + name_len > entries_buf.len() {
511 return Err(Error::Corrupt("xattr entry name overruns region"));
512 }
513 let name_bytes = entries_buf[pos + 16..pos + 16 + name_len].to_vec();
514 let value = if value_inum != 0 || value_size == 0 {
515 Vec::new()
516 } else {
517 if value_offs + value_size > entries_buf.len() {
518 return Err(Error::Corrupt("xattr value out of range"));
519 }
520 entries_buf[value_offs..value_offs + value_size].to_vec()
521 };
522 out.push(DecodedEntry {
523 name_index,
524 name_bytes,
525 value,
526 value_inum,
527 });
528 pos += (16 + name_len + 3) & !3;
529 }
530 Ok(out)
531}
532
533fn encode_in_inode_entries(region: &mut [u8], entries: &[DecodedEntry]) {
542 for b in region.iter_mut() {
543 *b = 0;
544 }
545 region[..4].copy_from_slice(&EXT4_XATTR_MAGIC.to_le_bytes());
546 let entries_area = &mut region[4..];
547 let area_len = entries_area.len();
548
549 let mut sorted: Vec<&DecodedEntry> = entries.iter().collect();
551 sorted.sort_by(|a, b| {
552 a.name_index
553 .cmp(&b.name_index)
554 .then_with(|| a.name_bytes.cmp(&b.name_bytes))
555 });
556
557 let mut entry_cursor: usize = 0;
558 let mut value_cursor: usize = area_len;
559
560 for e in &sorted {
561 let name_len = e.name_bytes.len();
562 let entry_padded = (16 + name_len + 3) & !3;
563
564 let value_offs = if e.value.is_empty() {
565 0
566 } else {
567 let value_padded = (e.value.len() + 3) & !3;
568 value_cursor -= value_padded;
569 entries_area[value_cursor..value_cursor + e.value.len()].copy_from_slice(&e.value);
570 value_cursor
571 };
572
573 entries_area[entry_cursor] = name_len as u8;
574 entries_area[entry_cursor + 1] = e.name_index;
575 entries_area[entry_cursor + 2..entry_cursor + 4]
576 .copy_from_slice(&(value_offs as u16).to_le_bytes());
577 entries_area[entry_cursor + 8..entry_cursor + 12]
579 .copy_from_slice(&(e.value.len() as u32).to_le_bytes());
580 entries_area[entry_cursor + 16..entry_cursor + 16 + name_len]
582 .copy_from_slice(&e.name_bytes);
583 entry_cursor += entry_padded;
584 }
585 }
587
588#[derive(Debug, Clone, Copy, PartialEq, Eq)]
594pub enum BlockRemoveOutcome {
595 Removed,
597 RemovedNowEmpty,
600 NotFound,
602}
603
604fn decode_external_block_entries(block: &[u8]) -> Result<Vec<DecodedEntry>> {
608 if block.len() < 32 {
609 return Err(Error::Corrupt("xattr block too small"));
610 }
611 let mut out = Vec::new();
612 let mut pos = 32usize;
613 while pos + 16 <= block.len() {
614 let header = u32::from_le_bytes(block[pos..pos + 4].try_into().unwrap());
615 if header == 0 {
616 break;
617 }
618 let name_len = block[pos] as usize;
619 let name_index = block[pos + 1];
620 let value_offs = u16::from_le_bytes(block[pos + 2..pos + 4].try_into().unwrap()) as usize;
621 let value_inum = u32::from_le_bytes(block[pos + 4..pos + 8].try_into().unwrap());
622 let value_size = u32::from_le_bytes(block[pos + 8..pos + 12].try_into().unwrap()) as usize;
623 if pos + 16 + name_len > block.len() {
624 return Err(Error::Corrupt("xattr entry name overruns block"));
625 }
626 let name_bytes = block[pos + 16..pos + 16 + name_len].to_vec();
627 let value = if value_inum != 0 || value_size == 0 {
628 Vec::new()
629 } else {
630 if value_offs + value_size > block.len() {
631 return Err(Error::Corrupt("xattr block value out of range"));
632 }
633 block[value_offs..value_offs + value_size].to_vec()
634 };
635 out.push(DecodedEntry {
636 name_index,
637 name_bytes,
638 value,
639 value_inum,
640 });
641 pos += (16 + name_len + 3) & !3;
642 }
643 Ok(out)
644}
645
646fn encode_external_block(block: &mut [u8], entries: &[DecodedEntry], refcount: u32) {
661 block.fill(0);
662 block[0x00..0x04].copy_from_slice(&EXT4_XATTR_MAGIC.to_le_bytes());
663 block[0x04..0x08].copy_from_slice(&refcount.to_le_bytes());
664 block[0x08..0x0C].copy_from_slice(&1u32.to_le_bytes());
665
666 let mut sorted: Vec<&DecodedEntry> = entries.iter().collect();
674 sorted.sort_by(|a, b| {
675 a.name_index
676 .cmp(&b.name_index)
677 .then_with(|| a.name_bytes.len().cmp(&b.name_bytes.len()))
678 .then_with(|| a.name_bytes.cmp(&b.name_bytes))
679 });
680
681 let mut entry_cursor: usize = 0x20;
682 let mut value_cursor: usize = block.len();
683 let mut hashes = Vec::with_capacity(sorted.len());
684
685 for e in &sorted {
686 let name_len = e.name_bytes.len();
687 let entry_padded = (16 + name_len + 3) & !3;
688
689 let value_offs = if e.value.is_empty() {
690 0
691 } else {
692 let value_padded = (e.value.len() + 3) & !3;
693 value_cursor -= value_padded;
694 block[value_cursor..value_cursor + e.value.len()].copy_from_slice(&e.value);
695 value_cursor
696 };
697
698 let e_hash = entry_hash(&e.name_bytes, &e.value);
701 hashes.push(e_hash);
702
703 block[entry_cursor] = name_len as u8;
704 block[entry_cursor + 1] = e.name_index;
705 block[entry_cursor + 2..entry_cursor + 4]
706 .copy_from_slice(&(value_offs as u16).to_le_bytes());
707 block[entry_cursor + 8..entry_cursor + 12]
709 .copy_from_slice(&(e.value.len() as u32).to_le_bytes());
710 block[entry_cursor + 12..entry_cursor + 16].copy_from_slice(&e_hash.to_le_bytes());
711 block[entry_cursor + 16..entry_cursor + 16 + name_len].copy_from_slice(&e.name_bytes);
712 entry_cursor += entry_padded;
713 }
714 block[0x0C..0x10].copy_from_slice(&block_hash(&hashes).to_le_bytes());
715 }
717
718const NAME_BYTE_ROTATION: u32 = 5;
747const WORD_ROTATION: u32 = 16;
749
750fn entry_hash(name: &[u8], value: &[u8]) -> u32 {
753 let after_name = name.iter().fold(0u32, |acc, &byte| {
754 acc.rotate_left(NAME_BYTE_ROTATION) ^ u32::from(byte)
755 });
756 value.chunks(4).fold(after_name, |acc, piece| {
757 let mut word = [0u8; 4];
758 word[..piece.len()].copy_from_slice(piece);
759 acc.rotate_left(WORD_ROTATION) ^ u32::from_le_bytes(word)
760 })
761}
762
763fn block_hash(entry_hashes: &[u32]) -> u32 {
766 if entry_hashes.contains(&0) {
767 return 0;
768 }
769 entry_hashes
770 .iter()
771 .fold(0u32, |acc, &hash| acc.rotate_left(WORD_ROTATION) ^ hash)
772}
773
774pub fn plan_set_in_external_block(
789 block: &mut [u8],
790 name: &str,
791 value: &[u8],
792 refcount: u32,
793) -> Result<SetOutcome> {
794 let Some((name_index, suffix)) = split_qualified_name(name) else {
795 return Err(Error::InvalidArgument(
796 "xattr name missing known namespace prefix",
797 ));
798 };
799 if suffix.is_empty() && !matches!(name_index, 2 | 3) {
800 return Err(Error::InvalidArgument("xattr name suffix is empty"));
801 }
802 if suffix.len() > 255 {
803 return Err(Error::NameTooLong);
804 }
805 if block.len() < 0x40 {
806 return Err(Error::NoSpaceLeftOnDevice);
807 }
808
809 let magic_present = u32::from_le_bytes(block[..4].try_into().unwrap()) == EXT4_XATTR_MAGIC;
810 let mut entries = if magic_present {
811 decode_external_block_entries(block)?
812 } else {
813 Vec::new()
814 };
815 refuse_if_any_ea_inode_backed(&entries)?;
816
817 let mut outcome = SetOutcome::Inserted;
818 let suffix_bytes = suffix.as_bytes();
819 for e in entries.iter_mut() {
820 if e.name_index == name_index && e.name_bytes == suffix_bytes {
821 e.value = value.to_vec();
822 outcome = SetOutcome::Replaced;
823 break;
824 }
825 }
826 if matches!(outcome, SetOutcome::Inserted) {
827 entries.push(DecodedEntry {
828 name_index,
829 name_bytes: suffix_bytes.to_vec(),
830 value: value.to_vec(),
831 value_inum: 0,
832 });
833 }
834
835 let entries_capacity = block.len() - 0x20;
836 let needed_entries: usize = entries
837 .iter()
838 .map(|e| (16 + e.name_bytes.len() + 3) & !3)
839 .sum();
840 let needed_values: usize = entries
841 .iter()
842 .filter(|e| !e.value.is_empty())
843 .map(|e| (e.value.len() + 3) & !3)
844 .sum();
845 if needed_entries + 4 + needed_values > entries_capacity {
846 return Err(Error::NoSpaceLeftOnDevice);
847 }
848
849 encode_external_block(block, &entries, refcount);
850 Ok(outcome)
851}
852
853pub fn plan_remove_from_external_block(
860 block: &mut [u8],
861 name: &str,
862 refcount: u32,
863) -> Result<BlockRemoveOutcome> {
864 let Some((name_index, suffix)) = split_qualified_name(name) else {
865 return Err(Error::InvalidArgument(
866 "xattr name missing known namespace prefix",
867 ));
868 };
869 if block.len() < 0x20 {
870 return Ok(BlockRemoveOutcome::NotFound);
871 }
872 let magic = u32::from_le_bytes(block[..4].try_into().unwrap());
873 if magic != EXT4_XATTR_MAGIC {
874 return Ok(BlockRemoveOutcome::NotFound);
875 }
876
877 let entries = decode_external_block_entries(block)?;
878 refuse_if_any_ea_inode_backed(&entries)?;
879 let before = entries.len();
880 let kept: Vec<DecodedEntry> = entries
881 .into_iter()
882 .filter(|e| !(e.name_index == name_index && e.name_bytes == suffix.as_bytes()))
883 .collect();
884 if kept.len() == before {
885 return Ok(BlockRemoveOutcome::NotFound);
886 }
887 if kept.is_empty() {
888 return Ok(BlockRemoveOutcome::RemovedNowEmpty);
889 }
890 encode_external_block(block, &kept, refcount);
891 Ok(BlockRemoveOutcome::Removed)
892}
893
894pub(crate) fn check_external_block(
902 csum: &crate::checksum::Checksummer,
903 block_nr: u64,
904 block: &[u8],
905) -> Result<()> {
906 if block.len() < 0x20 || u32::from_le_bytes(block[..4].try_into().unwrap()) != EXT4_XATTR_MAGIC
907 {
908 return Err(Error::Corrupt(
909 "i_file_acl names a block without the xattr magic",
910 ));
911 }
912 if u32::from_le_bytes(block[8..12].try_into().unwrap()) != 1 {
913 return Err(Error::Corrupt("xattr block h_blocks is not 1"));
914 }
915 if !csum.verify_xattr_block(block_nr, block) {
916 return Err(Error::BadChecksum {
917 what: "xattr block",
918 });
919 }
920 Ok(())
921}
922
923fn check_block_of(fs: &Filesystem, inode: &Inode) -> Result<()> {
928 if inode.file_acl == 0 {
929 return Ok(());
930 }
931 let bs = fs.sb.block_size();
932 let mut block = vec![0u8; bs as usize];
933 fs.dev.read_at(inode.file_acl * bs as u64, &mut block)?;
934 check_external_block(&fs.csum, inode.file_acl, &block)
935}
936
937pub fn get(
939 dev: &dyn BlockDevice,
940 inode: &Inode,
941 inode_raw: &[u8],
942 inode_size: u16,
943 block_size: u32,
944 name: &str,
945) -> Result<Option<Vec<u8>>> {
946 let all = read_all(dev, inode, inode_raw, inode_size, block_size)?;
947 Ok(all.into_iter().find(|e| e.name == name).map(|e| e.value))
948}
949
950pub fn read_all_resolved(
968 fs: &Filesystem,
969 inode: &Inode,
970 inode_raw: &[u8],
971) -> Result<Vec<XattrEntry>> {
972 check_block_of(fs, inode)?;
973 let mut entries = read_all(
974 fs.dev.as_ref(),
975 inode,
976 inode_raw,
977 fs.sb.inode_size,
978 fs.sb.block_size(),
979 )?;
980 for e in entries.iter_mut() {
981 if e.value_inum != 0 {
982 e.value = crate::ea_inode::read_value_inode(fs, e.value_inum, e.value_size)?;
983 }
984 }
985 Ok(entries)
986}
987
988pub fn list_names(fs: &Filesystem, inode: &Inode, inode_raw: &[u8]) -> Result<Vec<String>> {
996 check_block_of(fs, inode)?;
997 Ok(read_all(
998 fs.dev.as_ref(),
999 inode,
1000 inode_raw,
1001 fs.sb.inode_size,
1002 fs.sb.block_size(),
1003 )?
1004 .into_iter()
1005 .map(|e| e.name)
1006 .collect())
1007}
1008
1009pub fn get_resolved(
1012 fs: &Filesystem,
1013 inode: &Inode,
1014 inode_raw: &[u8],
1015 name: &str,
1016) -> Result<Option<Vec<u8>>> {
1017 check_block_of(fs, inode)?;
1018 let Some(entry) = read_all(
1019 fs.dev.as_ref(),
1020 inode,
1021 inode_raw,
1022 fs.sb.inode_size,
1023 fs.sb.block_size(),
1024 )?
1025 .into_iter()
1026 .find(|e| e.name == name) else {
1027 return Ok(None);
1028 };
1029 if entry.value_inum != 0 {
1030 return Ok(Some(crate::ea_inode::read_value_inode(
1031 fs,
1032 entry.value_inum,
1033 entry.value_size,
1034 )?));
1035 }
1036 Ok(Some(entry.value))
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041 use super::*;
1042
1043 #[test]
1044 fn prefixes_known() {
1045 assert_eq!(prefix_for_index(1), Some("user."));
1046 assert_eq!(prefix_for_index(7), Some("system."));
1047 assert_eq!(prefix_for_index(99), None);
1048 }
1049
1050 #[test]
1051 fn split_qualified_name_roundtrip() {
1052 assert_eq!(split_qualified_name("user.color"), Some((1, "color")));
1053 assert_eq!(
1054 split_qualified_name("user.com.apple.FinderInfo"),
1055 Some((1, "com.apple.FinderInfo"))
1056 );
1057 assert_eq!(
1058 split_qualified_name("security.selinux"),
1059 Some((6, "selinux"))
1060 );
1061 assert_eq!(split_qualified_name("unknown.foo"), None);
1062 }
1063
1064 #[test]
1069 fn new_builds_what_the_parser_reads_for_an_inline_value() {
1070 let mut region = vec![0u8; 64];
1071 encode_in_inode_entries(
1072 &mut region,
1073 &[DecodedEntry {
1074 name_index: 1,
1075 name_bytes: b"color".to_vec(),
1076 value: b"red".to_vec(),
1077 value_inum: 0,
1078 }],
1079 );
1080 let mut parsed = Vec::new();
1081 parse_entries(®ion[4..], region.len() - 4, &mut parsed).unwrap();
1082 assert_eq!(parsed, vec![XattrEntry::new("user.color", b"red".to_vec())]);
1083 }
1084
1085 #[test]
1089 fn in_ea_inode_carries_the_inode_and_declared_size_and_no_bytes() {
1090 let e = XattrEntry::in_ea_inode("user.big", 42, 70_000);
1091 assert_eq!(e.name, "user.big");
1092 assert!(e.value.is_empty());
1093 assert_eq!((e.value_inum, e.value_size), (42, 70_000));
1094 assert_ne!(e, XattrEntry::new("user.big", Vec::new()));
1095 }
1096
1097 #[test]
1100 fn remove_in_inode_roundtrips_one_of_two() {
1101 let mut region = vec![0u8; 96];
1104 let entries = vec![
1105 DecodedEntry {
1106 name_index: 1,
1107 name_bytes: b"color".to_vec(),
1108 value: b"red".to_vec(),
1109 value_inum: 0,
1110 },
1111 DecodedEntry {
1112 name_index: 1,
1113 name_bytes: b"mood".to_vec(),
1114 value: b"happy".to_vec(),
1115 value_inum: 0,
1116 },
1117 ];
1118 encode_in_inode_entries(&mut region, &entries);
1119
1120 let decoded = decode_in_inode_entries(®ion[4..]).unwrap();
1122 assert_eq!(decoded.len(), 2);
1123
1124 let outcome = plan_remove_in_inode_region(&mut region, "user.color").unwrap();
1125 assert_eq!(outcome, RemoveOutcome::Removed);
1126
1127 let after = decode_in_inode_entries(®ion[4..]).unwrap();
1128 assert_eq!(after.len(), 1);
1129 assert_eq!(after[0].name_bytes, b"mood");
1130 assert_eq!(after[0].value, b"happy");
1131 }
1132
1133 #[test]
1134 fn remove_in_inode_returns_not_found_for_missing() {
1135 let mut region = vec![0u8; 64];
1136 let entries = vec![DecodedEntry {
1137 name_index: 1,
1138 name_bytes: b"color".to_vec(),
1139 value: b"red".to_vec(),
1140 value_inum: 0,
1141 }];
1142 encode_in_inode_entries(&mut region, &entries);
1143 let outcome = plan_remove_in_inode_region(&mut region, "user.mood").unwrap();
1144 assert_eq!(outcome, RemoveOutcome::NotFound);
1145 }
1146
1147 #[test]
1148 fn remove_in_inode_unknown_prefix_is_einval() {
1149 let mut region = vec![0u8; 64];
1150 let err = plan_remove_in_inode_region(&mut region, "nope.name").unwrap_err();
1151 assert!(matches!(err, Error::InvalidArgument(_)));
1152 }
1153
1154 #[test]
1155 fn remove_in_inode_missing_magic_is_not_found() {
1156 let mut region = vec![0u8; 64];
1157 let outcome = plan_remove_in_inode_region(&mut region, "user.x").unwrap();
1159 assert_eq!(outcome, RemoveOutcome::NotFound);
1160 }
1161
1162 #[test]
1163 fn set_in_inode_inserts_new_into_empty_region() {
1164 let mut region = vec![0u8; 64];
1165 let outcome = plan_set_in_inode_region(&mut region, "user.color", b"red").unwrap();
1166 assert_eq!(outcome, SetOutcome::Inserted);
1167 let decoded = decode_in_inode_entries(®ion[4..]).unwrap();
1168 assert_eq!(decoded.len(), 1);
1169 assert_eq!(decoded[0].name_bytes, b"color");
1170 assert_eq!(decoded[0].value, b"red");
1171 }
1172
1173 #[test]
1174 fn set_in_inode_replaces_existing_value() {
1175 let mut region = vec![0u8; 96];
1176 plan_set_in_inode_region(&mut region, "user.color", b"red").unwrap();
1177 let outcome = plan_set_in_inode_region(&mut region, "user.color", b"emerald").unwrap();
1178 assert_eq!(outcome, SetOutcome::Replaced);
1179 let decoded = decode_in_inode_entries(®ion[4..]).unwrap();
1180 assert_eq!(decoded.len(), 1);
1181 assert_eq!(decoded[0].value, b"emerald");
1182 }
1183
1184 #[test]
1185 fn set_in_inode_preserves_other_entries() {
1186 let mut region = vec![0u8; 128];
1187 plan_set_in_inode_region(&mut region, "user.color", b"red").unwrap();
1188 plan_set_in_inode_region(&mut region, "user.mood", b"happy").unwrap();
1189 plan_set_in_inode_region(&mut region, "user.color", b"blue").unwrap();
1190 let decoded = decode_in_inode_entries(®ion[4..]).unwrap();
1191 let by_name: std::collections::BTreeMap<_, _> = decoded
1192 .into_iter()
1193 .map(|e| (e.name_bytes.clone(), e.value))
1194 .collect();
1195 assert_eq!(by_name.get(b"color".as_slice()).unwrap(), b"blue");
1196 assert_eq!(by_name.get(b"mood".as_slice()).unwrap(), b"happy");
1197 }
1198
1199 #[test]
1200 fn set_in_inode_enospc_on_overflow() {
1201 let mut region = vec![0u8; 32];
1202 let err =
1203 plan_set_in_inode_region(&mut region, "user.x", b"this_is_20_bytes_xx!").unwrap_err();
1204 assert!(matches!(err, Error::NoSpaceLeftOnDevice));
1205 }
1206
1207 #[test]
1208 fn set_in_inode_unknown_prefix_is_einval() {
1209 let mut region = vec![0u8; 64];
1210 let err = plan_set_in_inode_region(&mut region, "weird.key", b"v").unwrap_err();
1211 assert!(matches!(err, Error::InvalidArgument(_)));
1212 }
1213
1214 #[test]
1221 fn external_block_entries_are_sorted_by_name_length_before_name() {
1222 let mut block = vec![0u8; 4096];
1223 plan_set_in_external_block(&mut block, "user.abc", b"1", 1).unwrap();
1224 plan_set_in_external_block(&mut block, "user.zz", b"2", 1).unwrap();
1225 assert_eq!(
1226 block[0x20],
1227 2,
1228 "the first entry must be the shorter name (zz), not {:?}",
1229 String::from_utf8_lossy(&block[0x30..0x30 + block[0x20] as usize])
1230 );
1231 plan_set_in_external_block(&mut block, "trusted.a", b"3", 1).unwrap();
1234 let order: Vec<(u8, Vec<u8>)> = decode_external_block_entries(&block)
1235 .unwrap()
1236 .into_iter()
1237 .map(|e| (e.name_index, e.name_bytes))
1238 .collect();
1239 assert_eq!(
1240 order,
1241 vec![
1242 (1, b"zz".to_vec()),
1243 (1, b"abc".to_vec()),
1244 (4, b"a".to_vec()),
1245 ]
1246 );
1247 }
1248}
1249
1250#[cfg(test)]
1253mod hash_tests {
1254 use super::{block_hash, entry_hash, NAME_BYTE_ROTATION, WORD_ROTATION};
1255
1256 const OBSERVED: &[(&[u8], &[u8], u32)] = &[
1259 (b"a", b"x", 0x0061_0078),
1260 (b"bb", b"hello", 0x6568_6021),
1261 (b"empty", b"", 0x0667_4EF9),
1262 (b"z", b"0123456789", 0x067C_3F3E),
1263 (b"q", b"1", 0x0071_0031),
1264 (b"a", b"hi", 0x0061_6968),
1265 ("q\u{e9}".as_bytes(), b"val", 0xDCA5_6177),
1266 (b"a", &[0x00, 0x00, 0x61, 0x00], 0),
1268 (b"zzz", &[0x01, 0x00, 0x3A, 0xE7], 0),
1269 ];
1270
1271 #[test]
1272 fn entry_hashes_match_the_observed_words() {
1273 for &(name, value, want) in OBSERVED {
1274 assert_eq!(
1275 entry_hash(name, value),
1276 want,
1277 "{:?}={value:?}",
1278 String::from_utf8_lossy(name)
1279 );
1280 }
1281 }
1282
1283 #[test]
1286 fn the_block_hash_folds_the_entry_hashes_in_order() {
1287 let hashes = [0x0061_0078, 0x6568_6021, 0x0667_4EF9, 0x067C_3F3E];
1288 assert_eq!(block_hash(&hashes), 0x2D95_5919);
1289 assert_eq!(
1290 block_hash(&hashes[..1]),
1291 hashes[0],
1292 "one entry is its own hash"
1293 );
1294 assert_eq!(block_hash(&[]), 0);
1295 }
1296
1297 #[test]
1298 fn a_zero_entry_hash_anywhere_zeroes_the_block_hash() {
1299 assert_eq!(block_hash(&[0, 0x6568_6021]), 0);
1300 assert_eq!(block_hash(&[0x0061_6968, 0]), 0);
1301 }
1302
1303 fn sign_extending_entry_hash(name: &[u8], value: &[u8]) -> u32 {
1306 let after_name = name.iter().fold(0u32, |acc, &byte| {
1307 acc.rotate_left(NAME_BYTE_ROTATION) ^ (i32::from(byte as i8) as u32)
1308 });
1309 value.chunks(4).fold(after_name, |acc, piece| {
1310 let mut word = [0u8; 4];
1311 word[..piece.len()].copy_from_slice(piece);
1312 acc.rotate_left(WORD_ROTATION) ^ u32::from_le_bytes(word)
1313 })
1314 }
1315
1316 #[test]
1320 fn a_high_name_byte_has_two_readings() {
1321 for &(name, value, _) in OBSERVED {
1322 if name.iter().all(|&byte| byte <= 0x7F) {
1323 assert_eq!(
1324 sign_extending_entry_hash(name, value),
1325 entry_hash(name, value)
1326 );
1327 }
1328 }
1329 let name = "q\u{e9}".as_bytes();
1330 assert_eq!(entry_hash(name, b"val"), 0xDCA5_6177);
1331 assert_eq!(sign_extending_entry_hash(name, b"val"), 0xC3BA_6177);
1332 }
1333
1334 mod needs_host {
1336 use super::super::{
1337 block_hash, decode_external_block_entries, encode_external_block, entry_hash,
1338 };
1339 use super::sign_extending_entry_hash;
1340 use crate::block_io::FileDevice;
1341 use crate::fs::Filesystem;
1342 use std::sync::Arc;
1343
1344 type AttributeSet = (&'static str, &'static [(&'static str, &'static [u8])]);
1349
1350 const FILES: &[AttributeSet] = &[
1351 (
1352 "plain",
1353 &[
1354 ("user.a", b"x"),
1355 ("user.bb", b"hello"),
1356 ("user.empty", b""),
1357 ("trusted.z", b"0123456789"),
1358 ("user.q\u{e9}", b"val"),
1359 ],
1360 ),
1361 (
1362 "zero",
1363 &[("user.a", b"hi"), ("user.zzz", &[0x01, 0x00, 0x3A, 0xE7])],
1364 ),
1365 ];
1366
1367 fn volume(tag: &str) -> String {
1368 let image = fs_ext4_test_support::temp_path!(
1369 "fs_ext4_xattr_hash_{tag}_{}.img",
1370 std::process::id()
1371 );
1372 std::fs::File::create(&image)
1373 .and_then(|f| f.set_len(8 << 20))
1374 .unwrap();
1375 let out = fs_ext4_test_support::oracle("mkfs.ext4")
1378 .args(["-q", "-F", "-b", "1024", "-I", "128", &image])
1379 .output();
1380 assert!(
1381 out.status.success(),
1382 "{}",
1383 String::from_utf8_lossy(&out.stderr)
1384 );
1385 image
1386 }
1387
1388 fn hex(bytes: &[u8]) -> String {
1389 bytes.iter().map(|b| format!("{b:02x}")).collect()
1390 }
1391
1392 fn block_hashes(fs: &Filesystem, path: &str) -> (Vec<u32>, u32, Vec<u8>) {
1395 let ino = fs.lookup_path_bytes(path.as_bytes()).unwrap();
1396 let inode = fs.read_inode_verified(ino).unwrap().0;
1397 assert_ne!(inode.file_acl, 0, "{path} has no external block");
1398 let block = fs.read_block(inode.file_acl).unwrap();
1399 let mut hashes = Vec::new();
1400 let mut at = 0x20;
1401 while u32::from_le_bytes(block[at..at + 4].try_into().unwrap()) != 0 {
1402 hashes.push(u32::from_le_bytes(
1403 block[at + 12..at + 16].try_into().unwrap(),
1404 ));
1405 at += (16 + block[at] as usize + 3) & !3;
1406 }
1407 let h_hash = u32::from_le_bytes(block[0x0C..0x10].try_into().unwrap());
1408 (hashes, h_hash, block)
1409 }
1410
1411 #[test]
1418 fn hashes_in_kernel_written_blocks_are_the_ones_computed_here() {
1419 let image = volume("kernel");
1420 let mut script = String::from("set -e\n");
1421 for (file, attrs) in FILES {
1422 script.push_str(&format!("touch \"$MNT/{file}\"\n"));
1423 for (name, value) in *attrs {
1424 script.push_str(&format!(
1425 "setfattr -n '{name}' -v 0x{} \"$MNT/{file}\"\n",
1426 hex(value)
1427 ));
1428 }
1429 }
1430 let script = script.replace("-v 0x \"", "\"");
1431 let out = fs_ext4_test_support::guest_kernel_write(&image, &script);
1432 assert!(
1433 out.status.success(),
1434 "{}",
1435 String::from_utf8_lossy(&out.stderr)
1436 );
1437
1438 let fs = Filesystem::mount(Arc::new(FileDevice::open(&image).unwrap())).unwrap();
1439 for (file, attrs) in FILES {
1440 let path = format!("/{file}");
1441 let (hashes, h_hash, block) = block_hashes(&fs, &path);
1442 assert_eq!(hashes.len(), attrs.len(), "{path}");
1443 let entries = decode_external_block_entries(&block).unwrap();
1444 assert_eq!(entries.len(), hashes.len(), "{path}");
1445 let computed: Vec<u32> = entries
1446 .iter()
1447 .map(|entry| entry_hash(&entry.name_bytes, &entry.value))
1448 .collect();
1449 for ((entry, &stored), &ours) in entries.iter().zip(&hashes).zip(&computed) {
1450 let name = String::from_utf8_lossy(&entry.name_bytes);
1451 if stored == ours {
1452 continue;
1453 }
1454 assert!(
1455 entry.name_bytes.iter().any(|&byte| byte > 0x7F),
1456 "{path} {name}: the kernel stored {stored:#010x}, and this crate \
1457 computes {ours:#010x}"
1458 );
1459 assert_eq!(
1460 sign_extending_entry_hash(&entry.name_bytes, &entry.value),
1461 stored,
1462 "{path} {name}: the kernel's hash is neither reading"
1463 );
1464 }
1465 assert_eq!(block_hash(&hashes), h_hash, "{path}'s h_hash");
1466
1467 let mut ours = vec![0u8; block.len()];
1468 encode_external_block(&mut ours, &entries, 1);
1469 assert_eq!(
1470 ours[0x0C..0x10],
1471 block_hash(&computed).to_le_bytes(),
1472 "{path}: h_hash re-encoded"
1473 );
1474 if computed == hashes {
1475 assert_eq!(
1476 ours[0x0C..0x10],
1477 block[0x0C..0x10],
1478 "{path}: h_hash re-encoded"
1479 );
1480 }
1481 }
1482 drop(fs);
1483 fs_ext4_test_support::assert_e2fsck_clean(&image, "the kernel's blocks");
1484 let _ = std::fs::remove_file(&image);
1485 }
1486
1487 #[test]
1491 fn blocks_this_crate_writes_carry_the_kernels_hashes() {
1492 let image = volume("ours");
1493 {
1494 let fs = Filesystem::mount(Arc::new(FileDevice::open_rw(&image).unwrap())).unwrap();
1495 for (file, attrs) in FILES {
1496 let path = format!("/{file}");
1497 fs.apply_create(&path, 0o644).unwrap();
1498 for (name, value) in *attrs {
1499 fs.apply_setxattr(&path, name, value).unwrap();
1500 }
1501 }
1502 }
1503 fs_ext4_test_support::assert_e2fsck_clean(&image, "this crate's blocks");
1504
1505 let expected: &[(&str, &[u32], u32)] = &[
1507 (
1508 "/plain",
1509 &[
1510 0x0061_0078,
1511 0x6568_6021,
1512 0xDCA5_6177,
1513 0x0667_4EF9,
1514 0x067C_3F3E,
1515 ],
1516 block_hash(&[
1517 0x0061_0078,
1518 0x6568_6021,
1519 0xDCA5_6177,
1520 0x0667_4EF9,
1521 0x067C_3F3E,
1522 ]),
1523 ),
1524 ("/zero", &[0x0061_6968, 0], 0),
1525 ];
1526 let fs = Filesystem::mount(Arc::new(FileDevice::open(&image).unwrap())).unwrap();
1527 for &(path, hashes, h_hash) in expected {
1528 let (got, got_h, _) = block_hashes(&fs, path);
1529 assert_eq!(got, hashes, "{path}");
1530 assert_eq!(got_h, h_hash, "{path}");
1531 }
1532 drop(fs);
1533 let _ = std::fs::remove_file(&image);
1534 }
1535 }
1536}