From 7c9836a1371297d5dfecc67244d9d137343601ff Mon Sep 17 00:00:00 2001 From: Glenn Hartmann Date: Sun, 8 Feb 2026 13:12:20 -0500 Subject: [PATCH] Add pattern for BTRFS Send Streams. --- README.md | 1 + patterns/btrfs_send_stream.hexpat | 170 ++++++++++++++++++ .../test_data/btrfs_send_stream.hexpat.bin | Bin 0 -> 1989 bytes 3 files changed, 171 insertions(+) create mode 100644 patterns/btrfs_send_stream.hexpat create mode 100644 tests/patterns/test_data/btrfs_send_stream.hexpat.bin diff --git a/README.md b/README.md index f21148ad..a7e9ff24 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | BIN | | [`patterns/selinux.hexpat`](patterns/selinux.pat) | SE Linux modules | | BINKA | | [`patterns/binka.hexpat`](patterns/binka.pat) | RAD Game Tools Bink Audio (BINKA) files | | BSON | `application/bson` | [`patterns/bson.hexpat`](patterns/bson.hexpat) | BSON (Binary JSON) format | +| BTRFS Send Stream | | [`patterns/btrfs_send_stream.hexpat`](patterns/btrfs_send_stream.hexpat) | BTRFS Send Stream format | | bplist | `application/x-bplist` | [`patterns/bplist.hexpat`](patterns/bplist.hexpat) | Apple's binary property list format (bplist) | | BSP | | [`patterns/bsp_goldsrc.hexpat`](patterns/bsp_goldsrc.hexpat) | GoldSrc engine maps format (used in Half-Life 1) | | BZIP3 | | [`patterns/bzip3.hexpat`](patterns/bzip3.hexpat) | Parses BZip3 compression (file format) by Kamila Szewczyk | diff --git a/patterns/btrfs_send_stream.hexpat b/patterns/btrfs_send_stream.hexpat new file mode 100644 index 00000000..5992df09 --- /dev/null +++ b/patterns/btrfs_send_stream.hexpat @@ -0,0 +1,170 @@ +#pragma author Glenn Hartmann +#pragma description BTRFS Send Stream format - see https://btrfs.readthedocs.io/en/latest/dev/dev-send-stream.html. Currently only supports version 1. + +#pragma magic [ 62 74 72 66 73 2D 73 74 72 65 61 6D ] @ 0x0 + +#pragma endian little + +import std.hash; +import std.io; +import std.mem; +import std.sys; +import type.guid; +import type.magic; +import type.time; + +// See https://btrfs.readthedocs.io/en/latest/dev/dev-send-stream.html#stream-version-1. +enum CommandType : u16 { + BTRFS_SEND_C_UNSPEC = 0, + BTRFS_SEND_C_SUBVOL = 1, + BTRFS_SEND_C_SNAPSHOT = 2, + BTRFS_SEND_C_MKFILE = 3, + BTRFS_SEND_C_MKDIR = 4, + BTRFS_SEND_C_MKNOD = 5, + BTRFS_SEND_C_MKFIFO = 6, + BTRFS_SEND_C_MKSOCK = 7, + BTRFS_SEND_C_SYMLINK = 8, + BTRFS_SEND_C_RENAME = 9, + BTRFS_SEND_C_LINK = 10, + BTRFS_SEND_C_UNLINK = 11, + BTRFS_SEND_C_RMDIR = 12, + BTRFS_SEND_C_SET_XATTR = 13, + BTRFS_SEND_C_REMOVE_XATTR = 14, + BTRFS_SEND_C_WRITE = 15, + BTRFS_SEND_C_CLONE = 16, + BTRFS_SEND_C_TRUNCATE = 17, + BTRFS_SEND_C_CHMOD = 18, + BTRFS_SEND_C_CHOWN = 19, + BTRFS_SEND_C_UTIMES = 20, + BTRFS_SEND_C_END = 21, + BTRFS_SEND_C_UPDATE_EXTENT = 22, +}; + +// See https://btrfs.readthedocs.io/en/latest/dev/dev-send-stream.html#attributes-tlv-types. +enum AttributeType : u16 { + BTRFS_SEND_A_UNSPEC = 0, + BTRFS_SEND_A_UUID = 1, + BTRFS_SEND_A_CTRANSID = 2, + BTRFS_SEND_A_INO = 3, + BTRFS_SEND_A_SIZE = 4, + BTRFS_SEND_A_MODE = 5, + BTRFS_SEND_A_UID = 6, + BTRFS_SEND_A_GID = 7, + BTRFS_SEND_A_RDEV = 8, + BTRFS_SEND_A_CTIME = 9, + BTRFS_SEND_A_MTIME = 10, + BTRFS_SEND_A_ATIME = 11, + BTRFS_SEND_A_OTIME = 12, + BTRFS_SEND_A_XATTR_NAME = 13, + BTRFS_SEND_A_XATTR_DATA = 14, + BTRFS_SEND_A_PATH = 15, + BTRFS_SEND_A_PATH_TO = 16, + BTRFS_SEND_A_PATH_LINK = 17, + BTRFS_SEND_A_FILE_OFFSET = 18, + BTRFS_SEND_A_DATA = 19, + BTRFS_SEND_A_CLONE_UUID = 20, + BTRFS_SEND_A_CLONE_CTRANSID = 21, + BTRFS_SEND_A_CLONE_PATH = 22, + BTRFS_SEND_A_CLONE_OFFSET = 23, + BTRFS_SEND_A_CLONE_LEN = 24, +}; + +struct TimeSpec { + u64 seconds; + u32 nanoseconds; +} [[format("format_time_spec")]]; + +// Formats the "seconds" part of the TimeSpec as a time64_t. +fn format_time_spec(TimeSpec ts) { + return type::impl::format_time_t(ts.seconds); +}; + +struct Attribute { + AttributeType type; + u16 length; + + u64 pre_data_pos = $; + match (type) { + (AttributeType::BTRFS_SEND_A_UNSPEC): std::error("got unspecified attribute type"); + (AttributeType::BTRFS_SEND_A_UUID): type::GUID uuid; + (AttributeType::BTRFS_SEND_A_CLONE_UUID): type::GUID clone_uuid; + (AttributeType::BTRFS_SEND_A_CTRANSID): u64 ctransid; + (AttributeType::BTRFS_SEND_A_INO): u64 ino; + (AttributeType::BTRFS_SEND_A_SIZE): u64 size; + (AttributeType::BTRFS_SEND_A_MODE): u64 mode; + (AttributeType::BTRFS_SEND_A_UID): u64 uid; + (AttributeType::BTRFS_SEND_A_GID): u64 gid; + (AttributeType::BTRFS_SEND_A_RDEV): u64 rdev; + (AttributeType::BTRFS_SEND_A_CTIME): TimeSpec ctime; + (AttributeType::BTRFS_SEND_A_MTIME): TimeSpec mtime; + (AttributeType::BTRFS_SEND_A_ATIME): TimeSpec atime; + (AttributeType::BTRFS_SEND_A_OTIME): TimeSpec otime; + (AttributeType::BTRFS_SEND_A_XATTR_NAME): char xattr_name[length]; + (AttributeType::BTRFS_SEND_A_XATTR_DATA): u8 xattr_data[length]; + (AttributeType::BTRFS_SEND_A_PATH): char path[length]; + (AttributeType::BTRFS_SEND_A_PATH_TO): char path_to[length]; + (AttributeType::BTRFS_SEND_A_PATH_LINK): char path_link[length]; + (AttributeType::BTRFS_SEND_A_FILE_OFFSET): u64 file_offset; + (AttributeType::BTRFS_SEND_A_DATA): u8 data[length]; + (AttributeType::BTRFS_SEND_A_CLONE_CTRANSID): u64 clone_ctransid; + (AttributeType::BTRFS_SEND_A_CLONE_PATH): char clone_path[length]; + (AttributeType::BTRFS_SEND_A_CLONE_OFFSET): u64 clone_offset; + (AttributeType::BTRFS_SEND_A_CLONE_LEN): u64 clone_len; + (_): std::error(std::format("unknown attribute type: {}", type)); + } + std::assert($ - pre_data_pos == length, + std::format("bad attribute length: expected {}, got {}", length, $ - pre_data_pos)); +}; + +// Length-defined array of |Attribute|s. As the name suggests, |ByteLength| is the array length in +// bytes, not in elements. +struct Attributes { + Attribute attributes[while(!std::mem::reached(addressof(this) + ByteLength))] [[inline]]; +}; + +// The actual command structure. This is intended to be embedded inside a |Command| and +// [[inline]]d, so the user will never see the type name. +struct CommandInternal { + u32 byte_length; + CommandType type; + u32 checksum; // CRC32C with initial seed 0 + Attributes attributes; +}; + +// Wrapper structure around a CommandInternal to verify the checksum. +struct Command { + CommandInternal command [[inline]]; + + std::mem::Section mySection = std::mem::create_section("checksum buffer"); + std::mem::set_section_size(mySection, sizeof(command)); + std::mem::copy_value_to_section(command, mySection, 0x0); + + // The checksum needs to be computed on the entire |command|, but with its |checksum| member + // zeroed out, so we have to copy the data into a Section and modify it. + CommandInternal section_command @ 0x0 in mySection; + std::assert(sizeof(command) == sizeof(section_command), + std::format("|section_command| is the wrong size: expected {}, got {}", + sizeof(command), sizeof(section_command))); + section_command.checksum = 0; + + // For some reason, running the crc32 on |section_command| directly always gets the wrong value. + u8 data[sizeof(command)] @ 0x0 in mySection; + u32 crc32c = std::hash::crc32(data, 0 /* init */, 0x1EDC6F41 /* poly */, + 0 /* xorout */, true /* reflect_in */, true /* reflect_out */); + std::assert(crc32c == command.checksum, + std::format("bad command checksum: expected {}, got {}", command.checksum, crc32c)); + + std::mem::delete_section(mySection); +}; + +struct SendStream { + type::Magic<"btrfs-stream"> magic; + padding[1]; + u32 version; + std::assert(version == 1, + std::format("Only version 1 is currently supported (got version {})", version)); + Command commands[while(!std::mem::eof())]; +}; + +SendStream send_stream @ 0x0; +std::assert(std::mem::eof(), "Parsing did not consume whole file."); diff --git a/tests/patterns/test_data/btrfs_send_stream.hexpat.bin b/tests/patterns/test_data/btrfs_send_stream.hexpat.bin new file mode 100644 index 0000000000000000000000000000000000000000..64d4a1cd90c217886ff93807b93a356764653bd4 GIT binary patch literal 1989 zcmYc)DM~BWEiNfaP0VFrWME+M0b(YGDyd0t_!&4Dit`c+40Vh085sl^5>0|W{0K02 z>O3-)?FH9v6DFWM4^TA-hydjqUfU#|Io9Gb)j0dy%Xur3MS=315NU==AkEJ(x0T<8 zA83oI5KsyRgc(v84$oPgG{wF=b>9!Yhwmak8b0G=Hs4)*F7^GAz4hM>J4B-upDIoK z{7dIp==An}hDBV+b`xyAd8 z_>I%=NuGp&yU*#K_8R;4t=^8KteZmF`Hy{uKVorwH-JQ2lln4NQYz7Gc)kAFT zAluE*1Hi9^zhD_xXVzt*#Maw-?`-;*Zl8@ZGK$z4lA|APJ2tXZ7QeE7OKSCN8w+v#T+9h*vb9qW&3s#$QTuhy-4+4lW?(-a(H zzh^9F_VkejI!~CPE70f{*a>Vvm%m^JxtoE39f>UnlgH+5+_;BnU5j zvseh00tMkiXpqU>vbO>SAu=BngvfkI5dQVq^%)vI#_%9yM+w5n|C9HjC;W z(q{mJ7sy2qLJ=wDNs~QVKGZJVo>=EqdU)8 zEqv5raCY~pNsu5EpT%|s>;(Kls5xo-4X`rIAiU(UFAXe3UJ!nAKAsK@9}{>Ga-amE z=KP0TC`v#9K ze~irj9#__cOwm36qkmmq+@~wz8IK@Ac=ynuEU**s2jS_yFDJpuFoW>a34Rf<6mk$6 z71yGqLJ%K06@vJXRQS(kk|H#GOyNPui4ugbU;oZWQ347=OGprgvj_8nY_~)Y!v8jh z_jkRpo78jR$DJMWZ6`jiO7y+`^g)I5y!k)2Ci~8q=wftg?-Q#J_pX1++590UN_ypQ zfr#r$)7A-{OkEV|;L9g|KE=~yt?2>7l#I`Z!*(0&On%F<>gmK4?ZqYY3^ZPC<$AOZ z5`+$%-@t{bCH^2>w@vLQ*rk|3c%rQHGFXbdAbhnq_cb(p%-})D1r0)BU@5}M(C_0V zh@wOQShpA&Q~@RV8S3Nu_ws}J=z+LCcz1q;WH^IX=-zFUl+Ks3o(fcdRIB;x%F3;G z{nl@F%vIq&&&KQi-9F>+5d-_%^H(&VmV4{NtS7hE^7zc)pR?ZPHg4T5?q>GwSNknp zu0`LL#XhlPf6{TeD)-f@;pon@8zm`%_$FZeJi6}l`GG8u?q|^AH0{L|0BYe7r~m)} literal 0 HcmV?d00001