Skip to content
API Documentation
BastionGuard™
hfs_attributes.cpp
Go to the documentation of this file.
1// hfs_attributes.cpp -- HFS+ Attributes file walker.
2
3#include "hfs_attributes.hpp"
4
5#include <algorithm>
6#include <cstring>
7#include <sstream>
8
9namespace hfs {
10
11namespace {
12
13// Build the UTF-16BE representation of a UTF-8 attribute name.
14// HFS+ attr names are ASCII in practice (e.g. "com.apple.decmpfs"),
15// so we don't bother with full UTF-8 decoding -- we just expand each
16// ASCII byte to 2 bytes (00 + byte).
17std::vector<unsigned char> utf8_to_utf16be_ascii(const std::string& s) {
18 std::vector<unsigned char> out(s.size() * 2);
19 for (size_t i = 0; i < s.size(); ++i) {
20 out[i * 2 + 0] = 0;
21 out[i * 2 + 1] = static_cast<unsigned char>(s[i]);
22 }
23 return out;
24}
25
26} // anonymous namespace
27
28bool find_inline_attribute(const std::vector<unsigned char>& attributes_btree_buffer,
29 uint32_t fileID,
30 const std::string& attr_name,
31 std::vector<unsigned char>& out,
32 std::vector<std::string>& logs) {
33 out.clear();
34 if (attributes_btree_buffer.size() < 14) return false;
35
36 // Step 1: parse the B-tree header (node 0)
38 if (!parse_node_descriptor(attributes_btree_buffer.data(), n0) ||
39 n0.kind != kBTHeaderNode) {
40 logs.push_back("HFS+ Attributes: node 0 is not a header node");
41 return false;
42 }
43 HeaderRec hdr;
44 if (!parse_header_rec(attributes_btree_buffer.data() + 14, hdr)) {
45 logs.push_back("HFS+ Attributes: B-tree header parse failed");
46 return false;
47 }
48
49 // Step 2: build the UTF-16BE search key (we only match by name + fileID;
50 // attribute records may also have startBlock != 0 for extent
51 // continuation records, but those are not Inline records)
52 const auto target_name_utf16be = utf8_to_utf16be_ascii(attr_name);
53
54 // Step 3: walk the tree looking for matching records
55 auto node_reader = [&](uint32_t node_index,
56 std::vector<unsigned char>& nodebuf) -> bool {
57 const uint64_t off = static_cast<uint64_t>(node_index) * hdr.nodeSize;
58 if (off + hdr.nodeSize > attributes_btree_buffer.size()) return false;
59 nodebuf.assign(attributes_btree_buffer.begin() + off,
60 attributes_btree_buffer.begin() + off + hdr.nodeSize);
61 return true;
62 };
63
64 bool found = false;
65 auto on_leaf = [&](const unsigned char* rec, uint16_t rec_len) -> bool {
66 // Attribute KEY layout (after the 2-byte keyLength):
67 // __be16 pad
68 // __be32 fileID
69 // __be32 startBlock
70 // __be16 attrNameLength
71 // UTF-16BE attrName[attrNameLength]
72 if (rec_len < 14) return true;
73 const uint16_t keyLength = be16(rec + 0);
74 if (keyLength < 12 || 2u + keyLength > rec_len) return true;
75 // pad at +2
76 const uint32_t rec_fileID = be32(rec + 4);
77 if (rec_fileID != fileID) return true;
78 // startBlock at +8 (we want startBlock=0 for the primary record)
79 const uint32_t rec_startBlock = be32(rec + 8);
80 if (rec_startBlock != 0) return true; // skip overflow records
81 const uint16_t rec_nameLen = be16(rec + 12);
82 if (rec_nameLen != target_name_utf16be.size() / 2) return true;
83 // Compare UTF-16BE name byte-by-byte
84 const size_t name_offset = 14;
85 const size_t name_bytes = static_cast<size_t>(rec_nameLen) * 2;
86 if (name_offset + name_bytes > 2u + keyLength) return true;
87 if (std::memcmp(rec + name_offset, target_name_utf16be.data(),
88 name_bytes) != 0) return true;
89
90 // Found a key match. Body starts AFTER the key, on a 2-byte
91 // alignment boundary.
92 const uint32_t key_total = 2u + keyLength;
93 const uint32_t body_off = (key_total + 1u) & ~1u;
94 if (body_off + 4 > rec_len) return true;
95
96 const uint32_t recordType = be32(rec + body_off);
97 if (recordType != kHFSPlusAttrInlineData) {
98 // ForkData or Extents -- not supported here
99 std::ostringstream m;
100 m << "HFS+ Attributes: attribute '" << attr_name
101 << "' on fileID " << fileID
102 << " uses non-inline storage (recordType=0x"
103 << std::hex << recordType << ") -- not supported";
104 logs.push_back(m.str());
105 return false; // stop walk: this is the only record we expected
106 }
107
108 // Inline record body layout:
109 // __be32 recordType (= 0x10) already read
110 // __be32 reserved[2] +4..+12
111 // __be32 attrSize +12
112 // attribute data[attrSize] +16
113 if (body_off + 16 > rec_len) return true;
114 const uint32_t attrSize = be32(rec + body_off + 12);
115 if (attrSize == 0) return false;
116 if (body_off + 16 + attrSize > rec_len) {
117 logs.push_back("HFS+ Attributes: inline data overruns record");
118 return false;
119 }
120 out.assign(rec + body_off + 16, rec + body_off + 16 + attrSize);
121 found = true;
122 return false; // stop walk -- we got what we wanted
123 };
124
125 std::vector<std::string> walk_logs;
126 std::string walk_err;
127 walk_btree(hdr, node_reader, on_leaf, walk_logs, walk_err);
128 for (auto& l : walk_logs) logs.push_back(std::move(l));
129 if (!walk_err.empty()) {
130 logs.push_back("HFS+ Attributes walk: " + walk_err);
131 }
132 return found;
133}
134
135} // namespace hfs
std::vector< unsigned char > utf8_to_utf16be_ascii(const std::string &s)
constexpr uint32_t kHFSPlusAttrInlineData
bool parse_node_descriptor(const unsigned char *buf, NodeDescriptor &out)
Definition hfs_btree.cpp:17
uint32_t be32(const unsigned char *p)
bool find_inline_attribute(const std::vector< unsigned char > &attributes_btree_buffer, uint32_t fileID, const std::string &attr_name, std::vector< unsigned char > &out, std::vector< std::string > &logs)
bool walk_btree(const HeaderRec &header, const NodeReader &read_node, const LeafRecordCallback &on_leaf, std::vector< std::string > &logs, std::string &error)
constexpr int8_t kBTHeaderNode
Definition hfs_btree.hpp:34
bool parse_header_rec(const unsigned char *buf, HeaderRec &out)
Definition hfs_btree.cpp:38
uint16_t be16(const unsigned char *p)
uint16_t nodeSize
Definition hfs_btree.hpp:69