Skip to content
API Documentation
BastionGuard™
hfs_catalog.cpp
Go to the documentation of this file.
1// hfs_catalog.cpp -- HFS+ catalog record body decoders.
2
3#include "hfs_catalog.hpp"
4#include "hfs_unicode.hpp"
5
6#include <algorithm>
7#include <cstring>
8
9namespace hfs {
10
11namespace {
12
13// HFSPlusBSDInfo at offset 0x20 within both folder and file bodies
14// (16 bytes):
15// __be32 ownerID
16// __be32 groupID
17// __u8 adminFlags
18// __u8 ownerFlags
19// __be16 fileMode
20// __be32 special
21BSDInfo parse_bsd_info(const unsigned char* p) {
22 BSDInfo b;
23 b.ownerID = be32(p + 0x00);
24 b.groupID = be32(p + 0x04);
25 b.adminFlags = p[0x08];
26 b.ownerFlags = p[0x09];
27 b.fileMode = be16(p + 0x0A);
28 b.special = be32(p + 0x0C);
29 return b;
30}
31
32// FinderInfo (16 bytes) -- we only care about fileType+fileCreator
33// for files. For folders the layout differs but we don't use it.
34FinderInfo16 parse_finder_info(const unsigned char* p) {
36 f.fileType = be32(p + 0x00);
37 f.fileCreator = be32(p + 0x04);
38 f.finderFlags = be16(p + 0x08);
39 return f;
40}
41
42} // anonymous namespace
43
44bool parse_catalog_folder(const unsigned char* body, uint32_t body_len,
45 CatalogFolder& out) {
46 if (body == nullptr || body_len < 86) return false;
47 // Body layout (after the 2-byte recordType already consumed):
48 // 0x00 __be16 flags
49 // 0x02 __be32 valence
50 // 0x06 __be32 folderID
51 // 0x0A __be32 createDate
52 // 0x0E __be32 contentModDate
53 // 0x12 __be32 attributeModDate
54 // 0x16 __be32 accessDate
55 // 0x1A __be32 backupDate
56 // 0x1E HFSPlusBSDInfo permissions (16)
57 // 0x2E userInfo (16)
58 // 0x3E ExtendedFolderInfo (16) -- skipped
59 // 0x4E __be32 textEncoding
60 // 0x52 __be32 reserved
61 // Total body = 86 bytes (not 88 -- the 2 bytes of recordType are
62 // separate from this "body" view).
63 out = {};
64 out.flags = be16(body + 0x00);
65 out.valence = be32(body + 0x02);
66 out.folderID = be32(body + 0x06);
67 out.createDate = be32(body + 0x0A);
68 out.contentModDate = be32(body + 0x0E);
69 out.attributeModDate = be32(body + 0x12);
70 out.accessDate = be32(body + 0x16);
71 out.backupDate = be32(body + 0x1A);
72 out.permissions = parse_bsd_info(body + 0x1E);
73 out.userInfo = parse_finder_info(body + 0x2E);
74 out.textEncoding = be32(body + 0x4E);
75 return true;
76}
77
78bool parse_catalog_file(const unsigned char* body, uint32_t body_len,
79 CatalogFile& out) {
80 if (body == nullptr || body_len < 246) return false;
81 // Body layout (after the 2-byte recordType):
82 // 0x00 __be16 flags
83 // 0x02 __be32 reserved1
84 // 0x06 __be32 fileID
85 // 0x0A __be32 createDate
86 // 0x0E __be32 contentModDate
87 // 0x12 __be32 attributeModDate
88 // 0x16 __be32 accessDate
89 // 0x1A __be32 backupDate
90 // 0x1E permissions (16)
91 // 0x2E userInfo (16)
92 // 0x3E ExtendedFileInfo (16)
93 // 0x4E __be32 textEncoding
94 // 0x52 __be32 reserved2
95 // 0x56 dataFork (80)
96 // 0xA6 resourceFork (80)
97 // Total body = 246 bytes.
98 out = {};
99 out.flags = be16(body + 0x00);
100 out.fileID = be32(body + 0x06);
101 out.createDate = be32(body + 0x0A);
102 out.contentModDate = be32(body + 0x0E);
103 out.attributeModDate = be32(body + 0x12);
104 out.accessDate = be32(body + 0x16);
105 out.backupDate = be32(body + 0x1A);
106 out.permissions = parse_bsd_info(body + 0x1E);
107 out.userInfo = parse_finder_info(body + 0x2E);
108 out.textEncoding = be32(body + 0x4E);
109 if (!parse_fork_data(body + 0x56, out.dataFork)) return false;
110 if (!parse_fork_data(body + 0xA6, out.resourceFork)) return false;
111 return true;
112}
113
114std::string catalog_name_to_host(const unsigned char* utf16be_name,
115 uint16_t name_units) {
116 // Use the unicode helper from session 1: utf16be -> utf8 + NFD->NFC
117 std::string nfc = hfs::hfs_filename_to_host(utf16be_name, name_units);
118 // hfs_filename_to_host already strips '/', NUL and Windows reserved chars.
119 // Defensive: if the result is empty (weird file), name it "_".
120 if (nfc.empty()) return "_";
121 // HFS+ allows colons in names (they're path separators on classic Mac);
122 // they map to '/' on the POSIX side which we already block. So '/' in
123 // the user-facing name on Mac actually corresponds to ':' on disk.
124 // Convert any embedded ':' -> '_' as additional safety.
125 std::replace(nfc.begin(), nfc.end(), ':', '_');
126 return nfc;
127}
128
129bool is_private_data_folder_name(const unsigned char* utf16be_name,
130 uint16_t name_units) {
131 // Folder name on disk is 4 NUL (UTF-16) units followed by
132 // "HFS+ Private Data" (17 ASCII chars).
133 if (name_units != kHFSPrivateDataNameLength) return false;
134 // First 4 code units (8 bytes) must be all zero.
135 for (int i = 0; i < 8; ++i) {
136 if (utf16be_name[i] != 0) return false;
137 }
138 // Remaining 17 code units must be ASCII "HFS+ Private Data"
139 static const char* expected = "HFS+ Private Data";
140 for (int i = 0; i < 17; ++i) {
141 const unsigned char hi = utf16be_name[8 + i * 2];
142 const unsigned char lo = utf16be_name[8 + i * 2 + 1];
143 if (hi != 0) return false;
144 if (lo != static_cast<unsigned char>(expected[i])) return false;
145 }
146 return true;
147}
148
149uint32_t parse_inode_filename(const unsigned char* utf16be_name,
150 uint16_t name_units) {
151 // "iNode" prefix: 5 ASCII chars + at least 1 digit
152 if (name_units < 6) return 0;
153 static const char* prefix = "iNode";
154 for (int i = 0; i < 5; ++i) {
155 const unsigned char hi = utf16be_name[i * 2];
156 const unsigned char lo = utf16be_name[i * 2 + 1];
157 if (hi != 0) return 0;
158 if (lo != static_cast<unsigned char>(prefix[i])) return 0;
159 }
160 // Decode the trailing digits
161 uint32_t num = 0;
162 for (uint16_t i = 5; i < name_units; ++i) {
163 const unsigned char hi = utf16be_name[i * 2];
164 const unsigned char lo = utf16be_name[i * 2 + 1];
165 if (hi != 0) return 0;
166 if (lo < '0' || lo > '9') return 0;
167 const uint32_t prev = num;
168 num = num * 10 + (lo - '0');
169 if (num < prev) return 0; // overflow
170 }
171 return num;
172}
173
174} // namespace hfs
BSDInfo parse_bsd_info(const unsigned char *p)
FinderInfo16 parse_finder_info(const unsigned char *p)
bool parse_catalog_folder(const unsigned char *body, uint32_t body_len, CatalogFolder &out)
std::string catalog_name_to_host(const unsigned char *utf16be_name, uint16_t name_units)
constexpr int kHFSPrivateDataNameLength
std::string hfs_filename_to_host(const unsigned char *src, size_t code_units)
bool parse_fork_data(const unsigned char *buf, ForkData &out)
bool parse_catalog_file(const unsigned char *body, uint32_t body_len, CatalogFile &out)
uint32_t be32(const unsigned char *p)
uint32_t parse_inode_filename(const unsigned char *utf16be_name, uint16_t name_units)
uint16_t be16(const unsigned char *p)
bool is_private_data_folder_name(const unsigned char *utf16be_name, uint16_t name_units)
uint8_t ownerFlags
uint32_t groupID
uint16_t fileMode
uint32_t special
uint32_t ownerID
uint8_t adminFlags
uint32_t attributeModDate
FinderInfo16 userInfo
uint32_t contentModDate
uint32_t textEncoding
ForkData resourceFork
FinderInfo16 userInfo
uint32_t attributeModDate