yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
oracle_rom_safety_preflight.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cctype>
6#include <cstdio>
7#include <cstdlib>
8#include <fstream>
9#include <sstream>
10#include <string>
11#include <vector>
12
13#ifdef __APPLE__
14#include <CommonCrypto/CommonDigest.h>
15#endif
16
17#include "absl/strings/str_format.h"
18#include "rom/rom.h"
22
23namespace yaze::zelda3 {
24
25namespace {
26
27void AddError(OracleRomSafetyPreflightResult* result, std::string code,
28 std::string message, absl::StatusCode status_code,
29 int room_id = -1) {
30 if (!result) {
31 return;
32 }
34 issue.code = std::move(code);
35 issue.message = std::move(message);
36 issue.status_code = status_code;
37 issue.room_id = room_id;
38 result->errors.push_back(std::move(issue));
39}
40
41} // namespace
42
44 if (errors.empty()) {
45 return absl::OkStatus();
46 }
47 const auto& first = errors.front();
48 if (errors.size() == 1) {
49 return absl::Status(first.status_code, first.message);
50 }
51 return absl::Status(first.status_code,
52 absl::StrFormat("%s (plus %d additional safety issue(s))",
53 first.message, errors.size() - 1));
54}
55
57 Rom* rom, const OracleRomSafetyPreflightOptions& options) {
59
60 if (!rom || !rom->is_loaded()) {
61 AddError(&result, "ORACLE_ROM_NOT_LOADED", "ROM not loaded",
62 absl::StatusCode::kInvalidArgument);
63 return result;
64 }
65
66 const std::size_t rom_size = rom->vector().size();
67
69 !HasWaterFillReservedRegion(rom_size)) {
70 AddError(&result, "ORACLE_WATER_FILL_REGION_MISSING",
71 "WaterFill reserved region not present in this ROM",
72 absl::StatusCode::kFailedPrecondition);
73 }
74
77 AddError(&result, "ORACLE_COLLISION_WRITE_REGION_MISSING",
78 "Custom collision write support not present in this ROM",
79 absl::StatusCode::kFailedPrecondition);
80 }
81
82 if (options.validate_water_fill_table &&
84 const auto& data = rom->vector();
85 const uint8_t zone_count =
86 data[static_cast<std::size_t>(kWaterFillTableStart)];
87 if (zone_count > 8) {
88 AddError(
89 &result, "ORACLE_WATER_FILL_HEADER_CORRUPT",
90 absl::StrFormat("WaterFill table header corrupted (zone_count=%u)",
91 zone_count),
92 absl::StatusCode::kFailedPrecondition);
93 }
94
95 auto zones_or = LoadWaterFillTable(rom);
96 if (!zones_or.ok()) {
97 AddError(&result, "ORACLE_WATER_FILL_TABLE_INVALID",
98 std::string(zones_or.status().message()),
99 zones_or.status().code());
100 }
101 }
102
103 if (options.validate_custom_collision_maps &&
105 const int max_errors = std::max(1, options.max_collision_errors);
106 int collision_errors = 0;
107 for (int room_id = 0; room_id < kNumberOfRooms; ++room_id) {
108 auto map_or = LoadCustomCollisionMap(rom, room_id);
109 if (map_or.ok()) {
110 continue;
111 }
112 AddError(&result, "ORACLE_COLLISION_POINTER_INVALID",
113 absl::StrFormat("Room 0x%02X: %s", room_id,
114 std::string(map_or.status().message()).c_str()),
115 map_or.status().code(), room_id);
116 ++collision_errors;
117 if (collision_errors >= max_errors) {
118 AddError(&result, "ORACLE_COLLISION_POINTER_INVALID_TRUNCATED",
119 absl::StrFormat(
120 "Collision pointer validation stopped after %d "
121 "errors (increase max_collision_errors to scan more)",
122 max_errors),
123 absl::StatusCode::kFailedPrecondition);
124 break;
125 }
126 }
127 }
128
129 // Check that game-mechanic-critical rooms have authored custom collision data.
130 // This is independent of the pointer-validity sweep above: a room can have a
131 // valid (null) pointer that loads successfully with has_data=false, meaning
132 // the room has not been authored. For rooms like the D3 prison escape room
133 // (which requires MinishSwitch collision geometry to function), missing data
134 // is a gameplay-blocking error.
135 if (!options.room_ids_requiring_custom_collision.empty() &&
137 for (int room_id : options.room_ids_requiring_custom_collision) {
138 if (room_id < 0 || room_id >= kNumberOfRooms) {
139 AddError(&result, "ORACLE_REQUIRED_ROOM_OUT_OF_RANGE",
140 absl::StrFormat(
141 "Required room 0x%02X is out of valid range (0..0x%02X)",
142 room_id, kNumberOfRooms - 1),
143 absl::StatusCode::kInvalidArgument);
144 continue;
145 }
146 auto map_or = LoadCustomCollisionMap(rom, room_id);
147 if (!map_or.ok()) {
148 AddError(&result, "ORACLE_REQUIRED_ROOM_MISSING_COLLISION",
149 absl::StrFormat(
150 "Required room 0x%02X: collision pointer invalid: %s",
151 room_id, std::string(map_or.status().message()).c_str()),
152 map_or.status().code(), room_id);
153 continue;
154 }
155 if (!map_or->has_data) {
156 AddError(
157 &result, "ORACLE_REQUIRED_ROOM_MISSING_COLLISION",
158 absl::StrFormat("Required room 0x%02X has no custom collision data "
159 "(room not authored)",
160 room_id),
161 absl::StatusCode::kFailedPrecondition, room_id);
162 }
163 }
164 }
165
166 return result;
167}
168
169namespace {
170
171// Convert a raw digest to a lowercase hex string.
172std::string DigestToHex(const unsigned char* digest, std::size_t len) {
173 std::string hex;
174 hex.reserve(len * 2);
175 for (std::size_t i = 0; i < len; ++i) {
176 hex += absl::StrFormat("%02x", digest[i]);
177 }
178 return hex;
179}
180
181std::string ExtractHexDigestLine(const std::string& output) {
182 std::istringstream lines(output);
183 std::string line;
184 while (std::getline(lines, line)) {
185 // sha256sum/shasum output: "<hash> <filename>"
186 // certutil output: hash on its own line
187 // Extract the first whitespace-delimited token from each line.
188 std::istringstream tokens(line);
189 std::string token;
190 if (!(tokens >> token))
191 continue;
192
193 // Validate: must be exactly 64 lowercase hex chars.
194 if (token.size() != 64)
195 continue;
196 bool valid = true;
197 for (unsigned char c : token) {
198 if (!std::isxdigit(c)) {
199 valid = false;
200 break;
201 }
202 }
203 if (!valid)
204 continue;
205
206 // Normalize to lowercase.
207 std::string hash;
208 hash.reserve(64);
209 for (unsigned char c : token) {
210 hash.push_back(static_cast<char>(std::tolower(c)));
211 }
212 return hash;
213 }
214 return {};
215}
216
217} // namespace
218
219absl::StatusOr<std::string> ComputeSha256(const std::string& file_path) {
220 std::ifstream file(file_path, std::ios::binary);
221 if (!file.is_open()) {
222 return absl::NotFoundError(
223 absl::StrFormat("Cannot open file for hashing: %s", file_path));
224 }
225
226#ifdef __APPLE__
227 // Use CommonCrypto on macOS (part of libSystem, no extra linking needed).
228 CC_SHA256_CTX ctx;
229 CC_SHA256_Init(&ctx);
230
231 std::array<char, 8192> buffer;
232 while (file.read(buffer.data(), buffer.size()) || file.gcount() > 0) {
233 CC_SHA256_Update(&ctx, buffer.data(), static_cast<CC_LONG>(file.gcount()));
234 }
235
236 unsigned char digest[CC_SHA256_DIGEST_LENGTH];
237 CC_SHA256_Final(digest, &ctx);
238 return DigestToHex(digest, CC_SHA256_DIGEST_LENGTH);
239#else
240 // Fallback: shell out to platform hash utilities.
241 file.close();
242
243 std::string command;
244#ifdef _WIN32
245 command = absl::StrFormat("certutil -hashfile \"%s\" SHA256", file_path);
246#else
247 // Try sha256sum first (Linux), then shasum -a 256 (BSD/macOS shell envs).
248 if (std::system("command -v sha256sum >/dev/null 2>&1") == 0) {
249 command = absl::StrFormat("sha256sum '%s'", file_path);
250 } else if (std::system("command -v shasum >/dev/null 2>&1") == 0) {
251 command = absl::StrFormat("shasum -a 256 '%s'", file_path);
252 } else {
253 return absl::UnavailableError(
254 "Neither sha256sum nor shasum found on this system");
255 }
256#endif
257
258#ifdef _WIN32
259 FILE* pipe = _popen(command.c_str(), "r");
260#else
261 FILE* pipe = popen(command.c_str(), "r");
262#endif
263 if (!pipe) {
264 return absl::InternalError("Failed to execute hash command");
265 }
266
267 std::array<char, 128> result_buf;
268 std::string output;
269 while (fgets(result_buf.data(), result_buf.size(), pipe) != nullptr) {
270 output += result_buf.data();
271 }
272
273#ifdef _WIN32
274 int status = _pclose(pipe);
275#else
276 int status = pclose(pipe);
277#endif
278 if (status != 0) {
279 return absl::InternalError(
280 absl::StrFormat("Hash command failed with status %d", status));
281 }
282
283 const std::string hash = ExtractHexDigestLine(output);
284 if (hash.empty()) {
285 return absl::InternalError("Unexpected hash command output format");
286 }
287 return hash;
288#endif
289}
290
291absl::Status VerifySha256(const std::string& file_path,
292 const std::string& expected_hash) {
293 auto actual_hash_or = ComputeSha256(file_path);
294 if (!actual_hash_or.ok()) {
295 return actual_hash_or.status();
296 }
297
298 const std::string& actual_hash = *actual_hash_or;
299 if (actual_hash != expected_hash) {
300 return absl::DataLossError(
301 absl::StrFormat("SHA-256 mismatch for %s: expected %s, got %s",
302 file_path, expected_hash, actual_hash));
303 }
304
305 return absl::OkStatus();
306}
307
308} // namespace yaze::zelda3
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
const auto & vector() const
Definition rom.h:143
bool is_loaded() const
Definition rom.h:132
std::string DigestToHex(const unsigned char *digest, std::size_t len)
void AddError(OracleRomSafetyPreflightResult *result, std::string code, std::string message, absl::StatusCode status_code, int room_id=-1)
Zelda 3 specific classes and functions.
absl::Status VerifySha256(const std::string &file_path, const std::string &expected_hash)
constexpr int kWaterFillTableStart
OracleRomSafetyPreflightResult RunOracleRomSafetyPreflight(Rom *rom, const OracleRomSafetyPreflightOptions &options)
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
constexpr bool HasWaterFillReservedRegion(std::size_t rom_size)
constexpr int kNumberOfRooms
absl::StatusOr< std::string > ComputeSha256(const std::string &file_path)
constexpr bool HasCustomCollisionWriteSupport(std::size_t rom_size)
absl::StatusOr< std::vector< WaterFillZoneEntry > > LoadWaterFillTable(Rom *rom)