SquashFS compressed filesystem reader in pure OCaml
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(E005/E010): extract helpers to shorten long functions

- squashfs_writer.ml: extract 8 helpers from finalize (356→~50 lines)
- squashfs.ml: extract per-type parsers from parse_inode (80→~20 lines)
- streaming_aead.ml: extract encrypt_segment, decrypt_segments helpers
- tar.ml: extract decode_next_longlink and decode_active helpers

+697 -386
+89 -85
lib/squashfs.ml
··· 662 662 let ext_device_body_size = Wire.Codec.wire_size ext_device_body_codec 663 663 let ext_ipc_body_size = Wire.Codec.wire_size ext_ipc_body_codec 664 664 665 + (* Per-inode-type body parsers *) 666 + let parse_dir_inode buf body_off data_len = 667 + if body_off + dir_body_size > data_len then 668 + failwith "directory inode truncated"; 669 + let b = Wire.Codec.decode dir_body_codec buf body_off in 670 + ( Inode_dir 671 + { 672 + start_block = b.db_start_block; 673 + nlink = b.db_nlink; 674 + file_size = b.db_file_size + 3; 675 + offset = b.db_offset; 676 + parent_inode = b.db_parent_inode; 677 + }, 678 + no_xattr_id ) 679 + 680 + let parse_ext_dir_inode buf body_off data_len = 681 + if body_off + ext_dir_body_size > data_len then 682 + failwith "extended directory inode truncated"; 683 + let b = Wire.Codec.decode ext_dir_body_codec buf body_off in 684 + ( Inode_dir 685 + { 686 + start_block = b.edb_start_block; 687 + nlink = b.edb_nlink; 688 + file_size = b.edb_file_size + 3; 689 + offset = b.edb_offset; 690 + parent_inode = b.edb_parent_inode; 691 + }, 692 + b.edb_xattr_id ) 693 + 694 + let parse_file_inode buf body_off data_len = 695 + if body_off + file_body_size > data_len then 696 + failwith "regular file inode truncated"; 697 + let b = Wire.Codec.decode file_body_codec buf body_off in 698 + let fragment = 699 + if b.fb_fragment >= 0x80000000 then b.fb_fragment - 0x100000000 700 + else b.fb_fragment 701 + in 702 + ( Inode_file 703 + { 704 + start_block = Int64.of_int b.fb_start_block; 705 + fragment; 706 + offset = b.fb_offset; 707 + file_size = Int64.of_int b.fb_file_size; 708 + block_sizes = [||]; 709 + }, 710 + no_xattr_id ) 711 + 712 + let parse_ext_file_inode buf body_off data_len = 713 + if body_off + ext_file_body_size > data_len then 714 + failwith "extended regular file inode truncated"; 715 + let b = Wire.Codec.decode ext_file_body_codec buf body_off in 716 + let fragment = 717 + if b.efb_fragment >= 0x80000000 then b.efb_fragment - 0x100000000 718 + else b.efb_fragment 719 + in 720 + ( Inode_file 721 + { 722 + start_block = b.efb_start_block; 723 + fragment; 724 + offset = b.efb_offset; 725 + file_size = b.efb_file_size; 726 + block_sizes = [||]; 727 + }, 728 + b.efb_xattr_id ) 729 + 730 + let parse_symlink_inode data buf body_off data_len ~is_extended = 731 + if body_off + symlink_body_size > data_len then 732 + failwith "symlink inode truncated"; 733 + let b = Wire.Codec.decode symlink_body_codec buf body_off in 734 + let target_size = b.slb_target_size in 735 + if target_size > max_symlink_target_size then 736 + Fmt.failwith "symlink target too large: %d" target_size 737 + else if body_off + symlink_body_size + target_size > data_len then 738 + failwith "symlink target extends beyond data" 739 + else 740 + let target = String.sub data (body_off + symlink_body_size) target_size in 741 + let xattr_id = 742 + if is_extended then 743 + let xattr_off = body_off + symlink_body_size + target_size in 744 + if xattr_off + 4 <= data_len then u32_le data xattr_off else no_xattr_id 745 + else no_xattr_id 746 + in 747 + (Inode_symlink { nlink = b.slb_nlink; target }, xattr_id) 748 + 665 749 let parse_inode _t data offset = 666 750 let data_len = String.length data in 667 751 if offset + inode_header_size > data_len then Error "inode header truncated" ··· 677 761 try 678 762 let inode_data, xattr_id = 679 763 match (inode_type, is_extended) with 680 - | Directory, false -> 681 - if body_off + dir_body_size > data_len then 682 - failwith "directory inode truncated"; 683 - let b = Wire.Codec.decode dir_body_codec buf body_off in 684 - ( Inode_dir 685 - { 686 - start_block = b.db_start_block; 687 - nlink = b.db_nlink; 688 - file_size = b.db_file_size + 3; 689 - offset = b.db_offset; 690 - parent_inode = b.db_parent_inode; 691 - }, 692 - no_xattr_id ) 693 - | Directory, true -> 694 - if body_off + ext_dir_body_size > data_len then 695 - failwith "extended directory inode truncated"; 696 - let b = Wire.Codec.decode ext_dir_body_codec buf body_off in 697 - ( Inode_dir 698 - { 699 - start_block = b.edb_start_block; 700 - nlink = b.edb_nlink; 701 - file_size = b.edb_file_size + 3; 702 - offset = b.edb_offset; 703 - parent_inode = b.edb_parent_inode; 704 - }, 705 - b.edb_xattr_id ) 706 - | Regular, false -> 707 - if body_off + file_body_size > data_len then 708 - failwith "regular file inode truncated"; 709 - let b = Wire.Codec.decode file_body_codec buf body_off in 710 - (* Interpret fragment as signed: uint32 0xFFFFFFFF → -1 *) 711 - let fragment = 712 - if b.fb_fragment >= 0x80000000 then 713 - b.fb_fragment - 0x100000000 714 - else b.fb_fragment 715 - in 716 - ( Inode_file 717 - { 718 - start_block = Int64.of_int b.fb_start_block; 719 - fragment; 720 - offset = b.fb_offset; 721 - file_size = Int64.of_int b.fb_file_size; 722 - block_sizes = [||]; 723 - }, 724 - no_xattr_id ) 725 - | Regular, true -> 726 - if body_off + ext_file_body_size > data_len then 727 - failwith "extended regular file inode truncated"; 728 - let b = Wire.Codec.decode ext_file_body_codec buf body_off in 729 - let fragment = 730 - if b.efb_fragment >= 0x80000000 then 731 - b.efb_fragment - 0x100000000 732 - else b.efb_fragment 733 - in 734 - ( Inode_file 735 - { 736 - start_block = b.efb_start_block; 737 - fragment; 738 - offset = b.efb_offset; 739 - file_size = b.efb_file_size; 740 - block_sizes = [||]; 741 - }, 742 - b.efb_xattr_id ) 764 + | Directory, false -> parse_dir_inode buf body_off data_len 765 + | Directory, true -> parse_ext_dir_inode buf body_off data_len 766 + | Regular, false -> parse_file_inode buf body_off data_len 767 + | Regular, true -> parse_ext_file_inode buf body_off data_len 743 768 | Symlink, _ -> 744 - if body_off + symlink_body_size > data_len then 745 - failwith "symlink inode truncated"; 746 - let b = Wire.Codec.decode symlink_body_codec buf body_off in 747 - let target_size = b.slb_target_size in 748 - if target_size > max_symlink_target_size then 749 - Fmt.failwith "symlink target too large: %d" target_size 750 - else if body_off + symlink_body_size + target_size > data_len 751 - then failwith "symlink target extends beyond data" 752 - else 753 - let target = 754 - String.sub data (body_off + symlink_body_size) target_size 755 - in 756 - let xattr_id = 757 - if is_extended then 758 - let xattr_off = 759 - body_off + symlink_body_size + target_size 760 - in 761 - if xattr_off + 4 <= data_len then u32_le data xattr_off 762 - else no_xattr_id 763 - else no_xattr_id 764 - in 765 - (Inode_symlink { nlink = b.slb_nlink; target }, xattr_id) 769 + parse_symlink_inode data buf body_off data_len ~is_extended 766 770 | (Block_device | Char_device), false -> 767 771 if body_off + device_body_size > data_len then 768 772 failwith "device inode truncated";
+19
lib/squashfs.mli
··· 253 253 val pp_superblock : Format.formatter -> superblock -> unit 254 254 (** Pretty-print superblock information. *) 255 255 256 + (** {1 Internal — Testing} *) 257 + 258 + (** These values are exposed for unit testing only. *) 259 + 260 + type ext_dir_body 261 + type ext_file_body 262 + type ext_device_body 263 + type ext_ipc_body 264 + 265 + val ext_dir_body_codec : ext_dir_body Wire.Codec.t 266 + val ext_file_body_codec : ext_file_body Wire.Codec.t 267 + val ext_device_body_codec : ext_device_body Wire.Codec.t 268 + val ext_ipc_body_codec : ext_ipc_body Wire.Codec.t 269 + 270 + val parse_xattr_entries : 271 + string -> int -> int -> ((string * string) list, string) result 272 + (** [parse_xattr_entries data pos count] parses [count] xattr key-value pairs 273 + from [data] starting at [pos]. *) 274 + 256 275 (** {1 Writer} *) 257 276 258 277 module Writer = Squashfs_writer
+296 -301
lib/squashfs_writer.ml
··· 555 555 } 556 556 buf offset 557 557 558 - (* Build the complete squashfs image *) 559 - let finalize t = 560 - let output = Buffer.create 65536 in 561 - 562 - (* Reserve space for superblock (96 bytes) *) 563 - Buffer.add_string output (String.make superblock_size '\000'); 564 - 565 - (* Collect all data blocks and build inode table *) 566 - let data_blocks = Buffer.create 65536 in 567 - let inode_table = Buffer.create 4096 in 568 - let directory_table = Buffer.create 4096 in 569 - let id_table = Buffer.create 64 in 570 - 571 - (* Write ID table (just uid=0, gid=0 for now) *) 572 - let id_buf = Bytes.create 4 in 573 - Wire.UInt32.set_le id_buf 0 0; 574 - Buffer.add_bytes id_table id_buf; 575 - 576 - (* Track inode positions *) 577 - let inode_positions = Hashtbl.create 64 in 578 - let current_inode = ref 1 in 579 - let current_data_block = ref 0L in 580 - 581 - (* First pass: write data blocks and collect file info *) 582 - let rec write_data_for_entry path entry = 583 - match entry with 584 - | File { data; _ } -> 585 - let start_block = !current_data_block in 586 - if String.length data > 0 then begin 587 - (* Compress the data *) 588 - let compressed = compress t data in 589 - let use_compressed = String.length compressed < String.length data in 590 - let block_data = if use_compressed then compressed else data in 591 - let header = 592 - if use_compressed then String.length block_data 593 - else String.length block_data lor 0x1000000 (* Uncompressed flag *) 594 - in 595 - (* Write block size as header *) 596 - let hdr = Bytes.create 4 in 597 - Wire.UInt32.set_le hdr 0 header; 598 - Buffer.add_bytes data_blocks hdr; 599 - Buffer.add_string data_blocks block_data; 600 - current_data_block := 601 - Int64.add !current_data_block 602 - (Int64.of_int (4 + String.length block_data)) 603 - end; 604 - Some (start_block, String.length data) 605 - | Dir { children; _ } -> 606 - List.iter 607 - (fun (name, child) -> 608 - ignore (write_data_for_entry (Filename.concat path name) child)) 609 - children; 610 - None 611 - | _ -> None 612 - in 613 - List.iter 614 - (fun (name, entry) -> ignore (write_data_for_entry name entry)) 615 - t.root; 616 - 617 - (* Helper: encode inode header + body into a buffer *) 618 - let encode_inode inode_type mode inode_number body_size encode_body = 619 - let buf = Bytes.create (inode_header_size + body_size) in 620 - encode_inode_header buf 0 inode_type mode 0 0 t.mtime inode_number; 621 - encode_body buf inode_header_size; 622 - buf 623 - in 624 - 625 - (* Second pass: write inodes *) 626 - let inode_block_start = ref 0 in 627 - let rec write_inode_for_entry parent_inode path entry = 628 - let inode_number = !current_inode in 629 - incr current_inode; 630 - let inode_offset = Buffer.length inode_table in 631 - Hashtbl.add inode_positions path 632 - { 633 - inode_number; 634 - block_offset = inode_offset; 635 - block_start = !inode_block_start; 636 - }; 558 + (* Helper: align a buffer to a power-of-2 page boundary *) 559 + let align_to buf boundary = 560 + let len = Buffer.length buf in 561 + let padding = (boundary - (len mod boundary)) mod boundary in 562 + Buffer.add_string buf (String.make padding '\000') 637 563 638 - match entry with 639 - | Dir { mode; children } -> 640 - let nlink = List.length children + 2 in 641 - let file_size = 642 - List.fold_left 643 - (fun acc (n, _) -> acc + 8 + String.length n) 644 - 3 children 564 + (* First pass: compress and write data blocks for file entries *) 565 + let rec write_data_for_entry t data_blocks current_data_block path entry = 566 + match entry with 567 + | File { data; _ } -> 568 + let start_block = !current_data_block in 569 + if String.length data > 0 then begin 570 + let compressed = compress t data in 571 + let use_compressed = String.length compressed < String.length data in 572 + let block_data = if use_compressed then compressed else data in 573 + let header = 574 + if use_compressed then String.length block_data 575 + else String.length block_data lor 0x1000000 645 576 in 646 - let buf = 647 - encode_inode Basic_directory mode inode_number dir_body_size 648 - (fun buf off -> 649 - Wire.Codec.encode dir_body_codec 650 - { 651 - db_start_block = 0; 652 - db_nlink = nlink; 653 - db_file_size = file_size - 3; 654 - db_offset = 0; 655 - db_parent_inode = parent_inode; 656 - } 657 - buf off) 658 - in 659 - Buffer.add_bytes inode_table buf; 577 + let hdr = Bytes.create 4 in 578 + Wire.UInt32.set_le hdr 0 header; 579 + Buffer.add_bytes data_blocks hdr; 580 + Buffer.add_string data_blocks block_data; 581 + current_data_block := 582 + Int64.add !current_data_block 583 + (Int64.of_int (4 + String.length block_data)) 584 + end; 585 + Some (start_block, String.length data) 586 + | Dir { children; _ } -> 587 + List.iter 588 + (fun (name, child) -> 589 + ignore 590 + (write_data_for_entry t data_blocks current_data_block 591 + (Filename.concat path name) 592 + child)) 593 + children; 594 + None 595 + | _ -> None 660 596 661 - (* Process children *) 662 - List.iter 663 - (fun (name, child) -> 664 - ignore 665 - (write_inode_for_entry inode_number 666 - (Filename.concat path name) 667 - child)) 668 - children; 669 - inode_number 670 - | File { mode; data } -> 671 - (* fragment = -1 → unsigned 0xFFFFFFFF *) 672 - let fragment_unsigned = 0xFFFFFFFF in 673 - let buf = 674 - encode_inode Basic_file mode inode_number file_body_size 675 - (fun buf off -> 676 - Wire.Codec.encode file_body_codec 677 - { 678 - fb_start_block = 0; 679 - fb_fragment = fragment_unsigned; 680 - fb_offset = 0; 681 - fb_file_size = String.length data; 682 - } 683 - buf off) 684 - in 685 - Buffer.add_bytes inode_table buf; 686 - inode_number 687 - | Symlink { target } -> 688 - let target_len = String.length target in 689 - let buf = 690 - Bytes.create (inode_header_size + symlink_body_size + target_len) 691 - in 692 - encode_inode_header buf 0 Basic_symlink 0o777 0 0 t.mtime inode_number; 693 - Wire.Codec.encode symlink_body_codec 694 - { slb_nlink = 1; slb_target_size = target_len } 695 - buf inode_header_size; 696 - Bytes.blit_string target 0 buf 697 - (inode_header_size + symlink_body_size) 698 - target_len; 699 - Buffer.add_bytes inode_table buf; 700 - inode_number 701 - | Block_device { mode; major; minor } -> 702 - let rdev = (major lsl 8) lor minor in 703 - let buf = 704 - encode_inode Basic_block_device mode inode_number device_body_size 705 - (fun buf off -> 706 - Wire.Codec.encode device_body_codec 707 - { devb_nlink = 1; devb_rdev = rdev } 708 - buf off) 709 - in 710 - Buffer.add_bytes inode_table buf; 711 - inode_number 712 - | Char_device { mode; major; minor } -> 713 - let rdev = (major lsl 8) lor minor in 714 - let buf = 715 - encode_inode Basic_char_device mode inode_number device_body_size 716 - (fun buf off -> 717 - Wire.Codec.encode device_body_codec 718 - { devb_nlink = 1; devb_rdev = rdev } 719 - buf off) 720 - in 721 - Buffer.add_bytes inode_table buf; 722 - inode_number 723 - | Fifo { mode } -> 724 - let buf = 725 - encode_inode Basic_fifo mode inode_number ipc_body_size 726 - (fun buf off -> 727 - Wire.Codec.encode ipc_body_codec { ipcb_nlink = 1 } buf off) 728 - in 729 - Buffer.add_bytes inode_table buf; 730 - inode_number 731 - | Socket { mode } -> 732 - let buf = 733 - encode_inode Basic_socket mode inode_number ipc_body_size 734 - (fun buf off -> 735 - Wire.Codec.encode ipc_body_codec { ipcb_nlink = 1 } buf off) 736 - in 737 - Buffer.add_bytes inode_table buf; 738 - inode_number 739 - in 597 + (* Encode an inode header + body into a fresh buffer *) 598 + let make_inode_buf t inode_type mode inode_number body_size encode_body = 599 + let buf = Bytes.create (inode_header_size + body_size) in 600 + encode_inode_header buf 0 inode_type mode 0 0 t.mtime inode_number; 601 + encode_body buf inode_header_size; 602 + buf 740 603 741 - (* Write root directory inode first *) 742 - let root_inode = 743 - let inode_number = !current_inode in 744 - incr current_inode; 745 - let nlink = List.length t.root + 2 in 746 - let file_size = 747 - List.fold_left (fun acc (n, _) -> acc + 8 + String.length n) 3 t.root 748 - in 749 - let buf = 750 - encode_inode Basic_directory 0o755 inode_number dir_body_size 751 - (fun buf off -> 752 - Wire.Codec.encode dir_body_codec 753 - { 754 - db_start_block = 0; 755 - db_nlink = nlink; 756 - db_file_size = file_size - 3; 757 - db_offset = 0; 758 - db_parent_inode = inode_number; 759 - } 760 - buf off) 761 - in 762 - Buffer.add_bytes inode_table buf; 763 - Hashtbl.add inode_positions "/" 764 - { inode_number; block_offset = 0; block_start = 0 }; 765 - inode_number 766 - in 767 - 768 - (* Process all root entries *) 769 - List.iter 770 - (fun (name, entry) -> 771 - ignore (write_inode_for_entry root_inode ("/" ^ name) entry)) 772 - t.root; 773 - 774 - (* Third pass: write directory entries *) 775 - let rec write_dir_entries path children = 776 - if children <> [] then begin 777 - (* Directory header *) 778 - let header = Bytes.create dir_header_size in 779 - Wire.Codec.encode dir_header_codec 780 - { 781 - dh_count = List.length children - 1; 782 - dh_start_block = 0; 783 - dh_inode_number = root_inode; 784 - } 785 - header 0; 786 - Buffer.add_bytes directory_table header; 787 - 788 - (* Write entries *) 604 + (* Second pass: write inodes for all entries; returns the inode number *) 605 + let rec write_inode_for_entry t inode_table inode_positions current_inode 606 + inode_block_start parent_inode path entry = 607 + let inode_number = !current_inode in 608 + incr current_inode; 609 + let inode_offset = Buffer.length inode_table in 610 + Hashtbl.add inode_positions path 611 + { 612 + inode_number; 613 + block_offset = inode_offset; 614 + block_start = !inode_block_start; 615 + }; 616 + match entry with 617 + | Dir { mode; children } -> 618 + let nlink = List.length children + 2 in 619 + let file_size = 620 + List.fold_left (fun acc (n, _) -> acc + 8 + String.length n) 3 children 621 + in 622 + let buf = 623 + make_inode_buf t Basic_directory mode inode_number dir_body_size 624 + (fun buf off -> 625 + Wire.Codec.encode dir_body_codec 626 + { 627 + db_start_block = 0; 628 + db_nlink = nlink; 629 + db_file_size = file_size - 3; 630 + db_offset = 0; 631 + db_parent_inode = parent_inode; 632 + } 633 + buf off) 634 + in 635 + Buffer.add_bytes inode_table buf; 789 636 List.iter 790 - (fun (name, entry) -> 791 - let child_path = Filename.concat path name in 792 - let info = Hashtbl.find inode_positions child_path in 793 - let entry_type = 794 - match entry with 795 - | Dir _ -> 1 796 - | File _ -> 2 797 - | Symlink _ -> 3 798 - | Block_device _ -> 4 799 - | Char_device _ -> 5 800 - | Fifo _ -> 6 801 - | Socket _ -> 7 802 - in 803 - let name_len = String.length name in 804 - let entry_buf = Bytes.create (dir_entry_header_size + name_len) in 805 - Wire.Codec.encode dir_entry_header_codec 806 - { 807 - de_inode_offset = info.block_offset; 808 - de_inode_number = info.inode_number land 0xffff; 809 - de_entry_type = entry_type; 810 - de_name_size = name_len - 1; 811 - } 812 - entry_buf 0; 813 - Bytes.blit_string name 0 entry_buf dir_entry_header_size name_len; 814 - Buffer.add_bytes directory_table entry_buf; 637 + (fun (name, child) -> 638 + ignore 639 + (write_inode_for_entry t inode_table inode_positions current_inode 640 + inode_block_start inode_number 641 + (Filename.concat path name) 642 + child)) 643 + children; 644 + inode_number 645 + | File { mode; data } -> 646 + (* fragment = -1 represented as unsigned 0xFFFFFFFF *) 647 + let fragment_unsigned = 0xFFFFFFFF in 648 + let buf = 649 + make_inode_buf t Basic_file mode inode_number file_body_size 650 + (fun buf off -> 651 + Wire.Codec.encode file_body_codec 652 + { 653 + fb_start_block = 0; 654 + fb_fragment = fragment_unsigned; 655 + fb_offset = 0; 656 + fb_file_size = String.length data; 657 + } 658 + buf off) 659 + in 660 + Buffer.add_bytes inode_table buf; 661 + inode_number 662 + | Symlink { target } -> 663 + let target_len = String.length target in 664 + let buf = 665 + Bytes.create (inode_header_size + symlink_body_size + target_len) 666 + in 667 + encode_inode_header buf 0 Basic_symlink 0o777 0 0 t.mtime inode_number; 668 + Wire.Codec.encode symlink_body_codec 669 + { slb_nlink = 1; slb_target_size = target_len } 670 + buf inode_header_size; 671 + Bytes.blit_string target 0 buf 672 + (inode_header_size + symlink_body_size) 673 + target_len; 674 + Buffer.add_bytes inode_table buf; 675 + inode_number 676 + | Block_device { mode; major; minor } -> 677 + let rdev = (major lsl 8) lor minor in 678 + let buf = 679 + make_inode_buf t Basic_block_device mode inode_number device_body_size 680 + (fun buf off -> 681 + Wire.Codec.encode device_body_codec 682 + { devb_nlink = 1; devb_rdev = rdev } 683 + buf off) 684 + in 685 + Buffer.add_bytes inode_table buf; 686 + inode_number 687 + | Char_device { mode; major; minor } -> 688 + let rdev = (major lsl 8) lor minor in 689 + let buf = 690 + make_inode_buf t Basic_char_device mode inode_number device_body_size 691 + (fun buf off -> 692 + Wire.Codec.encode device_body_codec 693 + { devb_nlink = 1; devb_rdev = rdev } 694 + buf off) 695 + in 696 + Buffer.add_bytes inode_table buf; 697 + inode_number 698 + | Fifo { mode } -> 699 + let buf = 700 + make_inode_buf t Basic_fifo mode inode_number ipc_body_size 701 + (fun buf off -> 702 + Wire.Codec.encode ipc_body_codec { ipcb_nlink = 1 } buf off) 703 + in 704 + Buffer.add_bytes inode_table buf; 705 + inode_number 706 + | Socket { mode } -> 707 + let buf = 708 + make_inode_buf t Basic_socket mode inode_number ipc_body_size 709 + (fun buf off -> 710 + Wire.Codec.encode ipc_body_codec { ipcb_nlink = 1 } buf off) 711 + in 712 + Buffer.add_bytes inode_table buf; 713 + inode_number 815 714 816 - (* Recurse for directories *) 817 - match entry with 818 - | Dir { children; _ } -> write_dir_entries child_path children 819 - | _ -> ()) 820 - children 821 - end 715 + (* Write the root directory inode and record its position *) 716 + let write_root_inode t inode_table inode_positions current_inode = 717 + let inode_number = !current_inode in 718 + incr current_inode; 719 + let nlink = List.length t.root + 2 in 720 + let file_size = 721 + List.fold_left (fun acc (n, _) -> acc + 8 + String.length n) 3 t.root 822 722 in 823 - write_dir_entries "/" t.root; 824 - 825 - (* Align output buffer to 4K boundary *) 826 - let align_to buf boundary = 827 - let len = Buffer.length buf in 828 - let padding = (boundary - (len mod boundary)) mod boundary in 829 - Buffer.add_string buf (String.make padding '\000') 723 + let buf = 724 + make_inode_buf t Basic_directory 0o755 inode_number dir_body_size 725 + (fun buf off -> 726 + Wire.Codec.encode dir_body_codec 727 + { 728 + db_start_block = 0; 729 + db_nlink = nlink; 730 + db_file_size = file_size - 3; 731 + db_offset = 0; 732 + db_parent_inode = inode_number; 733 + } 734 + buf off) 830 735 in 831 - 832 - (* Write data blocks *) 833 - Buffer.add_buffer output data_blocks; 834 - align_to output 4096; 835 - 836 - (* Write compressed inode table *) 837 - let inode_table_start = Buffer.length output in 838 - let inode_meta = write_metadata_block t inode_table in 839 - Buffer.add_string output inode_meta; 840 - align_to output 4096; 841 - 842 - (* Write compressed directory table *) 843 - let directory_table_start = Buffer.length output in 844 - let dir_meta = write_metadata_block t directory_table in 845 - Buffer.add_string output dir_meta; 846 - align_to output 4096; 736 + Buffer.add_bytes inode_table buf; 737 + Hashtbl.add inode_positions "/" 738 + { inode_number; block_offset = 0; block_start = 0 }; 739 + inode_number 847 740 848 - (* Write fragment table (empty for now) *) 849 - let fragment_table_start = Int64.minus_one in 741 + (* Map an entry to its on-disk directory entry type integer *) 742 + let entry_type_of_entry = function 743 + | Dir _ -> 1 744 + | File _ -> 2 745 + | Symlink _ -> 3 746 + | Block_device _ -> 4 747 + | Char_device _ -> 5 748 + | Fifo _ -> 6 749 + | Socket _ -> 7 850 750 851 - (* Write export table (not used) *) 852 - let export_table_start = Int64.minus_one in 751 + (* Third pass: write directory headers and entries *) 752 + let rec write_dir_entries directory_table inode_positions root_inode path 753 + children = 754 + if children <> [] then begin 755 + let header = Bytes.create dir_header_size in 756 + Wire.Codec.encode dir_header_codec 757 + { 758 + dh_count = List.length children - 1; 759 + dh_start_block = 0; 760 + dh_inode_number = root_inode; 761 + } 762 + header 0; 763 + Buffer.add_bytes directory_table header; 764 + List.iter 765 + (fun (name, entry) -> 766 + let child_path = Filename.concat path name in 767 + let info = Hashtbl.find inode_positions child_path in 768 + let etype = entry_type_of_entry entry in 769 + let name_len = String.length name in 770 + let entry_buf = Bytes.create (dir_entry_header_size + name_len) in 771 + Wire.Codec.encode dir_entry_header_codec 772 + { 773 + de_inode_offset = info.block_offset; 774 + de_inode_number = info.inode_number land 0xffff; 775 + de_entry_type = etype; 776 + de_name_size = name_len - 1; 777 + } 778 + entry_buf 0; 779 + Bytes.blit_string name 0 entry_buf dir_entry_header_size name_len; 780 + Buffer.add_bytes directory_table entry_buf; 781 + match entry with 782 + | Dir { children; _ } -> 783 + write_dir_entries directory_table inode_positions root_inode 784 + child_path children 785 + | _ -> ()) 786 + children 787 + end 853 788 854 - (* Write ID table - format: 855 - 1. At id_table_start: array of 64-bit pointers to ID metadata blocks 856 - 2. Each ID metadata block has 2-byte header (size | 0x8000 for uncompressed) 857 - followed by the UID/GID values *) 789 + (* Write id_table metadata block and lookup pointer; return id_table_start *) 790 + let write_id_table output id_table = 858 791 let id_data_start = Buffer.length output in 859 - (* Write ID metadata block: 2-byte header + 4 bytes per ID *) 860 792 let id_data_size = Buffer.length id_table in 861 793 let id_header = Bytes.create 2 in 862 794 set_u16_le id_header 0 (id_data_size lor 0x8000); 863 - (* Uncompressed flag *) 864 795 Buffer.add_bytes output id_header; 865 796 Buffer.add_buffer output id_table; 866 - 867 - (* Write ID table lookup (array of pointers to ID blocks) *) 868 797 let id_table_start = Buffer.length output in 869 798 let id_ptr = Bytes.create 8 in 870 799 Wire.UInt32.set_le id_ptr 0 ··· 872 801 Wire.UInt32.set_le id_ptr 4 873 802 (Int64.to_int (Int64.shift_right_logical (Int64.of_int id_data_start) 32)); 874 803 Buffer.add_bytes output id_ptr; 875 - align_to output 4096; 804 + id_table_start 876 805 877 - (* Write xattr table (not used) *) 878 - let xattr_table_start = Int64.minus_one in 879 - 880 - let bytes_used = Buffer.length output in 881 - 882 - (* Now write the superblock *) 806 + (* Build and return the encoded superblock bytes *) 807 + let build_superblock t ~inode_table_start ~directory_table_start 808 + ~fragment_table_start ~export_table_start ~id_table_start ~xattr_table_start 809 + ~bytes_used = 883 810 let rec log2 n = if n <= 1 then 0 else 1 + log2 (n lsr 1) in 884 811 let st = stats t in 885 812 let sb = Bytes.create superblock_size in ··· 907 834 sb_export_table_start = export_table_start; 908 835 } 909 836 sb 0; 837 + sb 910 838 911 - (* Copy superblock to beginning of output *) 839 + (* Write data blocks for all root entries. *) 840 + let write_all_data t data_blocks current_data_block = 841 + List.iter 842 + (fun (name, entry) -> 843 + ignore (write_data_for_entry t data_blocks current_data_block name entry)) 844 + t.root 845 + 846 + (* Write inodes for all root entries. *) 847 + let write_all_inodes t inode_table inode_positions current_inode 848 + inode_block_start root_inode = 849 + List.iter 850 + (fun (name, entry) -> 851 + ignore 852 + (write_inode_for_entry t inode_table inode_positions current_inode 853 + inode_block_start root_inode ("/" ^ name) entry)) 854 + t.root 855 + 856 + (* Build the complete squashfs image *) 857 + let finalize t = 858 + let output = Buffer.create 65536 in 859 + Buffer.add_string output (String.make superblock_size '\000'); 860 + 861 + let data_blocks = Buffer.create 65536 in 862 + let inode_table = Buffer.create 4096 in 863 + let directory_table = Buffer.create 4096 in 864 + let id_table = Buffer.create 64 in 865 + let id_buf = Bytes.create 4 in 866 + Wire.UInt32.set_le id_buf 0 0; 867 + Buffer.add_bytes id_table id_buf; 868 + 869 + let inode_positions = Hashtbl.create 64 in 870 + let current_inode = ref 1 in 871 + let current_data_block = ref 0L in 872 + let inode_block_start = ref 0 in 873 + 874 + write_all_data t data_blocks current_data_block; 875 + let root_inode = 876 + write_root_inode t inode_table inode_positions current_inode 877 + in 878 + write_all_inodes t inode_table inode_positions current_inode inode_block_start 879 + root_inode; 880 + write_dir_entries directory_table inode_positions root_inode "/" t.root; 881 + 882 + Buffer.add_buffer output data_blocks; 883 + align_to output 4096; 884 + 885 + let inode_table_start = Buffer.length output in 886 + Buffer.add_string output (write_metadata_block t inode_table); 887 + align_to output 4096; 888 + 889 + let directory_table_start = Buffer.length output in 890 + Buffer.add_string output (write_metadata_block t directory_table); 891 + align_to output 4096; 892 + 893 + let fragment_table_start = Int64.minus_one in 894 + let export_table_start = Int64.minus_one in 895 + 896 + let id_table_start = write_id_table output id_table in 897 + align_to output 4096; 898 + 899 + let xattr_table_start = Int64.minus_one in 900 + let bytes_used = Buffer.length output in 901 + 902 + let sb = 903 + build_superblock t ~inode_table_start ~directory_table_start 904 + ~fragment_table_start ~export_table_start ~id_table_start 905 + ~xattr_table_start ~bytes_used 906 + in 912 907 let result = Buffer.to_bytes output in 913 908 Bytes.blit sb 0 result 0 superblock_size; 914 909 Bytes.to_string result
+293
test/test_squashfs.ml
··· 268 268 Alcotest.(check int64) 269 269 "export_table_start" sb.sb_export_table_start sb'.sb_export_table_start 270 270 271 + (* ---- xattr tests ---- *) 272 + 273 + (* Helper: build a minimal SquashFS image using the writer *) 274 + let make_image f = 275 + let fs = Squashfs.Writer.v () in 276 + f fs; 277 + match Squashfs.of_string (Squashfs.Writer.finalize fs) with 278 + | Ok t -> t 279 + | Error e -> Alcotest.failf "of_string failed: %s" e 280 + 281 + (* has_xattrs returns false for a plain image (no xattr table) *) 282 + let test_has_xattrs_false () = 283 + let t = 284 + make_image (fun fs -> 285 + Squashfs.Writer.add_file fs "hello.txt" ~mode:0o644 "hi") 286 + in 287 + Alcotest.(check bool) "no xattrs" false (Squashfs.has_xattrs t) 288 + 289 + (* xattr returns Ok None for inode with no xattrs (xattr_id = 0xFFFFFFFF) *) 290 + let test_xattr_no_xattr_table () = 291 + let t = 292 + make_image (fun fs -> Squashfs.Writer.add_file fs "f" ~mode:0o644 "data") 293 + in 294 + let root = Squashfs.root t in 295 + match Squashfs.xattr t root "user.foo" with 296 + | Ok None -> () 297 + | Ok (Some _) -> Alcotest.fail "expected None" 298 + | Error e -> Alcotest.failf "unexpected error: %s" e 299 + 300 + (* list_xattrs returns Ok [] for inode with no xattrs *) 301 + let test_list_xattrs_empty () = 302 + let t = 303 + make_image (fun fs -> Squashfs.Writer.add_directory fs "d" ~mode:0o755) 304 + in 305 + let root = Squashfs.root t in 306 + match Squashfs.list_xattrs t root with 307 + | Ok [] -> () 308 + | Ok _ -> Alcotest.fail "expected empty list" 309 + | Error e -> Alcotest.failf "unexpected error: %s" e 310 + 311 + (* xattr on a symlink inode with no xattrs returns Ok None *) 312 + let test_xattr_symlink_no_xattrs () = 313 + let t = 314 + make_image (fun fs -> 315 + Squashfs.Writer.add_file fs "target" ~mode:0o644 "x"; 316 + Squashfs.Writer.add_symlink fs "link" "target") 317 + in 318 + let root = Squashfs.root t in 319 + match Squashfs.xattr t root "user.test" with 320 + | Ok None -> () 321 + | Ok (Some _) -> Alcotest.fail "expected None" 322 + | Error e -> Alcotest.failf "unexpected error: %s" e 323 + 324 + (* Parse xattr entries from crafted binary data *) 325 + let test_parse_xattr_entries_user () = 326 + (* Build a single xattr entry: type=0 (user), name="hello", value="world" *) 327 + let name = "hello" in 328 + let value = "world" in 329 + let name_size = String.length name in 330 + let value_size = String.length value in 331 + let buf = Bytes.create (4 + name_size + 4 + value_size) in 332 + (* type = 0 (user), little-endian *) 333 + Bytes.set_uint16_le buf 0 0; 334 + (* name_size *) 335 + Bytes.set_uint16_le buf 2 name_size; 336 + (* name *) 337 + Bytes.blit_string name 0 buf 4 name_size; 338 + (* value_size, uint32 LE *) 339 + Bytes.set_uint16_le buf (4 + name_size) value_size; 340 + Bytes.set_uint16_le buf (4 + name_size + 2) 0; 341 + (* value *) 342 + Bytes.blit_string value 0 buf (4 + name_size + 4) value_size; 343 + let data = Bytes.to_string buf in 344 + match Squashfs.parse_xattr_entries data 0 1 with 345 + | Error e -> Alcotest.failf "parse error: %s" e 346 + | Ok [] -> Alcotest.fail "expected one entry" 347 + | Ok ((k, v) :: _) -> 348 + Alcotest.(check string) "key" "user.hello" k; 349 + Alcotest.(check string) "value" "world" v 350 + 351 + (* Parse xattr entries for trusted. prefix *) 352 + let test_parse_xattr_entries_trusted () = 353 + let name = "caps" in 354 + let value = "\x00\x00\x00\x01" in 355 + let name_size = String.length name in 356 + let value_size = String.length value in 357 + let buf = Bytes.create (4 + name_size + 4 + value_size) in 358 + Bytes.set_uint16_le buf 0 1; 359 + (* type = 1 = trusted *) 360 + Bytes.set_uint16_le buf 2 name_size; 361 + Bytes.blit_string name 0 buf 4 name_size; 362 + Bytes.set_uint16_le buf (4 + name_size) value_size; 363 + Bytes.set_uint16_le buf (4 + name_size + 2) 0; 364 + Bytes.blit_string value 0 buf (4 + name_size + 4) value_size; 365 + let data = Bytes.to_string buf in 366 + match Squashfs.parse_xattr_entries data 0 1 with 367 + | Error e -> Alcotest.failf "parse error: %s" e 368 + | Ok [] -> Alcotest.fail "expected one entry" 369 + | Ok ((k, v) :: _) -> 370 + Alcotest.(check string) "key" "trusted.caps" k; 371 + Alcotest.(check string) "value" value v 372 + 373 + (* Parse xattr entries for security. prefix *) 374 + let test_parse_xattr_entries_security () = 375 + let name = "selinux" in 376 + let value = "system_u:object_r:default_t:s0" in 377 + let name_size = String.length name in 378 + let value_size = String.length value in 379 + let buf = Bytes.create (4 + name_size + 4 + value_size) in 380 + Bytes.set_uint16_le buf 0 2; 381 + (* type = 2 = security *) 382 + Bytes.set_uint16_le buf 2 name_size; 383 + Bytes.blit_string name 0 buf 4 name_size; 384 + Bytes.set_uint16_le buf (4 + name_size) value_size; 385 + Bytes.set_uint16_le buf (4 + name_size + 2) 0; 386 + Bytes.blit_string value 0 buf (4 + name_size + 4) value_size; 387 + let data = Bytes.to_string buf in 388 + match Squashfs.parse_xattr_entries data 0 1 with 389 + | Error e -> Alcotest.failf "parse error: %s" e 390 + | Ok [] -> Alcotest.fail "expected one entry" 391 + | Ok ((k, v) :: _) -> 392 + Alcotest.(check string) "key" "security.selinux" k; 393 + Alcotest.(check string) "value" value v 394 + 395 + (* Parse multiple xattr entries *) 396 + let test_parse_xattr_multiple () = 397 + let entries_data = 398 + [ (0, "name", "value"); (1, "cap", "\x01\x00"); (2, "ctx", "unconfined") ] 399 + in 400 + let total_size = 401 + List.fold_left 402 + (fun acc (_, n, v) -> acc + 4 + String.length n + 4 + String.length v) 403 + 0 entries_data 404 + in 405 + let buf = Bytes.create total_size in 406 + let pos = ref 0 in 407 + List.iter 408 + (fun (t, name, value) -> 409 + let nl = String.length name and vl = String.length value in 410 + Bytes.set_uint16_le buf !pos t; 411 + Bytes.set_uint16_le buf (!pos + 2) nl; 412 + Bytes.blit_string name 0 buf (!pos + 4) nl; 413 + Bytes.set_uint16_le buf (!pos + 4 + nl) vl; 414 + Bytes.set_uint16_le buf (!pos + 4 + nl + 2) 0; 415 + Bytes.blit_string value 0 buf (!pos + 4 + nl + 4) vl; 416 + pos := !pos + 4 + nl + 4 + vl) 417 + entries_data; 418 + let data = Bytes.to_string buf in 419 + match Squashfs.parse_xattr_entries data 0 3 with 420 + | Error e -> Alcotest.failf "parse error: %s" e 421 + | Ok pairs -> 422 + Alcotest.(check int) "count" 3 (List.length pairs); 423 + Alcotest.(check string) "first key" "user.name" (fst (List.nth pairs 0)); 424 + Alcotest.(check string) 425 + "second key" "trusted.cap" 426 + (fst (List.nth pairs 1)); 427 + Alcotest.(check string) 428 + "third key" "security.ctx" 429 + (fst (List.nth pairs 2)) 430 + 431 + (* Truncated xattr header returns error *) 432 + let test_parse_xattr_truncated_header () = 433 + let data = "\x00\x01" in 434 + (* only 2 bytes, need 4 *) 435 + match Squashfs.parse_xattr_entries data 0 1 with 436 + | Error _ -> () (* expected *) 437 + | Ok _ -> Alcotest.fail "should fail on truncated header" 438 + 439 + (* Truncated xattr name returns error *) 440 + let test_parse_xattr_truncated_name () = 441 + let buf = Bytes.create 4 in 442 + Bytes.set_uint16_le buf 0 0; 443 + (* type = user *) 444 + Bytes.set_uint16_le buf 2 100; 445 + (* name_size = 100 but no name follows *) 446 + let data = Bytes.to_string buf in 447 + match Squashfs.parse_xattr_entries data 0 1 with 448 + | Error _ -> () 449 + | Ok _ -> Alcotest.fail "should fail on truncated name" 450 + 451 + (* Truncated xattr value size returns error *) 452 + let test_parse_xattr_truncated_value_size () = 453 + let name = "x" in 454 + let buf = Bytes.create (4 + 1) in 455 + (* header + 1 byte name, no value_size *) 456 + Bytes.set_uint16_le buf 0 0; 457 + Bytes.set_uint16_le buf 2 1; 458 + Bytes.set buf 4 'x'; 459 + ignore name; 460 + let data = Bytes.to_string buf in 461 + match Squashfs.parse_xattr_entries data 0 1 with 462 + | Error _ -> () 463 + | Ok _ -> Alcotest.fail "should fail on truncated value size" 464 + 465 + (* Truncated xattr value returns error *) 466 + let test_parse_xattr_truncated_value () = 467 + let name = "key" in 468 + let nl = String.length name in 469 + let buf = Bytes.create (4 + nl + 4) in 470 + Bytes.set_uint16_le buf 0 0; 471 + Bytes.set_uint16_le buf 2 nl; 472 + Bytes.blit_string name 0 buf 4 nl; 473 + Bytes.set_uint16_le buf (4 + nl) 100; 474 + (* value_size = 100 but no value *) 475 + Bytes.set_uint16_le buf (4 + nl + 2) 0; 476 + let data = Bytes.to_string buf in 477 + match Squashfs.parse_xattr_entries data 0 1 with 478 + | Error _ -> () 479 + | Ok _ -> Alcotest.fail "should fail on truncated value" 480 + 481 + (* Empty data returns error when count > 0 *) 482 + let test_parse_xattr_empty_data () = 483 + match Squashfs.parse_xattr_entries "" 0 1 with 484 + | Error _ -> () 485 + | Ok _ -> Alcotest.fail "should fail on empty data" 486 + 487 + (* count = 0 on empty data returns Ok [] *) 488 + let test_parse_xattr_zero_count () = 489 + match Squashfs.parse_xattr_entries "" 0 0 with 490 + | Error _ -> Alcotest.fail "should succeed with count=0" 491 + | Ok pairs -> Alcotest.(check int) "empty" 0 (List.length pairs) 492 + 493 + (* Malformed xattr_id_table returns error *) 494 + let test_xattr_bad_table_start () = 495 + (* A valid image normally has xattr_table_start = 0xFFFFFFFFFFFFFFFF (no xattrs). 496 + We can't easily craft one with a bad table, but we can test the has_xattrs path. *) 497 + let t = 498 + make_image (fun fs -> Squashfs.Writer.add_file fs "f" ~mode:0o644 "x") 499 + in 500 + (* has_xattrs is false for writer-generated images *) 501 + Alcotest.(check bool) "has_xattrs false" false (Squashfs.has_xattrs t); 502 + (* xattr on root (no xattr_id) is Ok None *) 503 + match Squashfs.xattr t (Squashfs.root t) "user.x" with 504 + | Ok None -> () 505 + | Ok (Some _) -> Alcotest.fail "unexpected value" 506 + | Error e -> Alcotest.failf "unexpected error: %s" e 507 + 508 + (* Extended codec sizes match expected wire format sizes *) 509 + let test_ext_dir_body_size () = 510 + Alcotest.(check int) 511 + "ext_dir_body = 24 bytes" 24 512 + (Wire.Codec.wire_size Squashfs.ext_dir_body_codec) 513 + 514 + let test_ext_file_body_size () = 515 + Alcotest.(check int) 516 + "ext_file_body = 40 bytes" 40 517 + (Wire.Codec.wire_size Squashfs.ext_file_body_codec) 518 + 519 + let test_ext_device_body_size () = 520 + Alcotest.(check int) 521 + "ext_device_body = 12 bytes" 12 522 + (Wire.Codec.wire_size Squashfs.ext_device_body_codec) 523 + 524 + let test_ext_ipc_body_size () = 525 + Alcotest.(check int) 526 + "ext_ipc_body = 8 bytes" 8 527 + (Wire.Codec.wire_size Squashfs.ext_ipc_body_codec) 528 + 271 529 let suite = 272 530 ( "squashfs", 273 531 [ ··· 296 554 Alcotest.test_case "superblock codec size" `Quick 297 555 test_superblock_codec_size; 298 556 Alcotest.test_case "superblock roundtrip" `Quick test_superblock_roundtrip; 557 + Alcotest.test_case "xattr: has_xattrs false" `Quick test_has_xattrs_false; 558 + Alcotest.test_case "xattr: no table, root" `Quick 559 + test_xattr_no_xattr_table; 560 + Alcotest.test_case "xattr: list empty" `Quick test_list_xattrs_empty; 561 + Alcotest.test_case "xattr: symlink no xattrs" `Quick 562 + test_xattr_symlink_no_xattrs; 563 + Alcotest.test_case "xattr: parse user." `Quick 564 + test_parse_xattr_entries_user; 565 + Alcotest.test_case "xattr: parse trusted." `Quick 566 + test_parse_xattr_entries_trusted; 567 + Alcotest.test_case "xattr: parse security." `Quick 568 + test_parse_xattr_entries_security; 569 + Alcotest.test_case "xattr: parse multiple" `Quick 570 + test_parse_xattr_multiple; 571 + Alcotest.test_case "xattr: truncated header" `Quick 572 + test_parse_xattr_truncated_header; 573 + Alcotest.test_case "xattr: truncated name" `Quick 574 + test_parse_xattr_truncated_name; 575 + Alcotest.test_case "xattr: truncated value size" `Quick 576 + test_parse_xattr_truncated_value_size; 577 + Alcotest.test_case "xattr: truncated value" `Quick 578 + test_parse_xattr_truncated_value; 579 + Alcotest.test_case "xattr: empty data count=1" `Quick 580 + test_parse_xattr_empty_data; 581 + Alcotest.test_case "xattr: zero count" `Quick test_parse_xattr_zero_count; 582 + Alcotest.test_case "xattr: bad table start" `Quick 583 + test_xattr_bad_table_start; 584 + Alcotest.test_case "xattr: ext_dir_body size" `Quick 585 + test_ext_dir_body_size; 586 + Alcotest.test_case "xattr: ext_file_body size" `Quick 587 + test_ext_file_body_size; 588 + Alcotest.test_case "xattr: ext_device_body size" `Quick 589 + test_ext_device_body_size; 590 + Alcotest.test_case "xattr: ext_ipc_body size" `Quick 591 + test_ext_ipc_body_size; 299 592 ] )