Skip to content

Commit de4fe7f

Browse files
committed
feat: add selfpath + bundle_locator for EOCD detection
Part of #40, depends on #41. Two small modules that let the running binary discover an appended ZIP bundle. - `src/selfpath.{hpp,cpp}` -- cross-platform self-binary path: - Linux: readlink("/proc/self/exe") - macOS: _NSGetExecutablePath - Windows: GetModuleFileNameW (with buffer growth loop) - `src/bundle_locator.{hpp,cpp}` -- reverse-scan a file for a ZIP End-of-Central-Directory record: - reads tail buffer of 22 + max_comment + 64 KiB padding budget - reverse-scans for the 0x06054b50 signature - validates: single-disk archive, entry counts match, comment length fits in tail, anything after comment must be zero (padding tolerance), central directory must fit before EOCD - returns BundleLocation{offset, size} or nullopt - `LocateBundleInSelf()` convenience wrapper over GetSelfPath() Tests (`test/cpp/bundle_locator_test.cpp`, 7 cases): - locate a ZIP appended to 4 KiB of random leading bytes - tolerate 10 KiB of trailing zero padding (the spike-caught case) - nullopt when no EOCD signature exists in random data - nullopt when an EOCD signature has impossible cd_size / cd_offset - nullopt when the bundle is truncated by 1 KiB from EOF - selfpath returns an existing path - LocateBundleInSelf returns nullopt against the unbundled test binary Fixtures reuse `archive_io::WriteArchive` (#41) to produce real ZIPs. Closes #42.
1 parent 716c885 commit de4fe7f

7 files changed

Lines changed: 441 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ add_library(flapi-lib STATIC
232232
src/api_server.cpp
233233
src/archive_io.cpp
234234
src/audit_logger.cpp
235+
src/bundle_locator.cpp
235236
src/auth_middleware.cpp
236237
src/cache_manager.cpp
237238
src/database_manager_cache_adapter.cpp
@@ -263,6 +264,7 @@ add_library(flapi-lib STATIC
263264
src/prepared_value_converter.cpp
264265
src/route_translator.cpp
265266
src/security_auditor.cpp
267+
src/selfpath.cpp
266268
src/sql_parameter_classifier.cpp
267269
src/sql_template_processor.cpp
268270
src/sql_utils.cpp

src/bundle_locator.cpp

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#include "bundle_locator.hpp"
2+
#include "selfpath.hpp"
3+
4+
#include <algorithm>
5+
#include <cstdint>
6+
#include <fstream>
7+
#include <system_error>
8+
#include <vector>
9+
10+
namespace flapi {
11+
12+
namespace {
13+
14+
constexpr std::size_t kEocdRecordSize = 22;
15+
constexpr std::size_t kMaxCommentLen = 0xffffu;
16+
17+
// We accept padding well in excess of the 10 KiB spike default; the
18+
// total tail buffer is EOCD + max comment + 64 KiB pad budget.
19+
constexpr std::size_t kPaddingBudget = 65536;
20+
constexpr std::size_t kScanBudget = kEocdRecordSize + kMaxCommentLen + kPaddingBudget;
21+
22+
std::uint16_t ReadU16(const std::uint8_t* p) {
23+
return static_cast<std::uint16_t>(
24+
static_cast<std::uint16_t>(p[0]) |
25+
(static_cast<std::uint16_t>(p[1]) << 8));
26+
}
27+
28+
std::uint32_t ReadU32(const std::uint8_t* p) {
29+
return static_cast<std::uint32_t>(p[0])
30+
| (static_cast<std::uint32_t>(p[1]) << 8)
31+
| (static_cast<std::uint32_t>(p[2]) << 16)
32+
| (static_cast<std::uint32_t>(p[3]) << 24);
33+
}
34+
35+
} // namespace
36+
37+
std::optional<BundleLocation> LocateBundle(const std::filesystem::path& path) {
38+
std::error_code ec;
39+
const auto file_size = std::filesystem::file_size(path, ec);
40+
if (ec || file_size < kEocdRecordSize) {
41+
return std::nullopt;
42+
}
43+
44+
std::ifstream in(path, std::ios::binary);
45+
if (!in.is_open()) {
46+
return std::nullopt;
47+
}
48+
49+
const std::size_t tail_bytes =
50+
static_cast<std::size_t>(std::min<std::uint64_t>(file_size, kScanBudget));
51+
const std::uint64_t tail_start = file_size - tail_bytes;
52+
53+
std::vector<std::uint8_t> tail(tail_bytes);
54+
in.seekg(static_cast<std::streamoff>(tail_start), std::ios::beg);
55+
in.read(reinterpret_cast<char*>(tail.data()),
56+
static_cast<std::streamsize>(tail_bytes));
57+
if (!in) {
58+
return std::nullopt;
59+
}
60+
if (tail.size() < kEocdRecordSize) {
61+
return std::nullopt;
62+
}
63+
64+
// Reverse-scan from the latest valid signature position. The latest
65+
// (largest-offset) EOCD wins, since any earlier signature byte
66+
// sequence in random leading data is a false positive.
67+
const std::size_t max_start = tail.size() - kEocdRecordSize;
68+
for (std::size_t i = max_start + 1; i-- > 0; ) {
69+
if (tail[i] != 0x50 ||
70+
tail[i + 1] != 0x4b ||
71+
tail[i + 2] != 0x05 ||
72+
tail[i + 3] != 0x06) {
73+
continue;
74+
}
75+
76+
const std::uint8_t* p = tail.data() + i;
77+
const std::uint16_t this_disk = ReadU16(p + 4);
78+
const std::uint16_t cd_start_disk = ReadU16(p + 6);
79+
const std::uint16_t entries_this = ReadU16(p + 8);
80+
const std::uint16_t entries_total = ReadU16(p + 10);
81+
const std::uint32_t cd_size = ReadU32(p + 12);
82+
const std::uint32_t cd_offset_arch = ReadU32(p + 16);
83+
const std::uint16_t comment_len = ReadU16(p + 20);
84+
85+
// Multi-disk archives are not supported.
86+
if (this_disk != 0 || cd_start_disk != 0) {
87+
continue;
88+
}
89+
if (entries_this != entries_total) {
90+
continue;
91+
}
92+
93+
// The comment must fit in the tail.
94+
const std::size_t comment_end = i + kEocdRecordSize + comment_len;
95+
if (comment_end > tail.size()) {
96+
continue;
97+
}
98+
99+
// Anything after the comment up to file-EOF must be zero
100+
// padding -- the libarchive tar-block rounding tolerance.
101+
bool padding_ok = true;
102+
for (std::size_t j = comment_end; j < tail.size(); ++j) {
103+
if (tail[j] != 0) {
104+
padding_ok = false;
105+
break;
106+
}
107+
}
108+
if (!padding_ok) {
109+
continue;
110+
}
111+
112+
const std::uint64_t eocd_file_offset = tail_start + i;
113+
114+
// The central directory sits immediately before the EOCD.
115+
if (cd_size > eocd_file_offset) {
116+
continue;
117+
}
118+
const std::uint64_t cd_file_offset = eocd_file_offset - cd_size;
119+
120+
if (cd_offset_arch > cd_file_offset) {
121+
continue;
122+
}
123+
const std::uint64_t bundle_start = cd_file_offset - cd_offset_arch;
124+
125+
const std::uint64_t bundle_end = eocd_file_offset + kEocdRecordSize + comment_len;
126+
if (bundle_end < bundle_start) {
127+
continue; // overflow paranoia
128+
}
129+
130+
BundleLocation loc;
131+
loc.offset = bundle_start;
132+
loc.size = bundle_end - bundle_start;
133+
return loc;
134+
}
135+
136+
return std::nullopt;
137+
}
138+
139+
std::optional<BundleLocation> LocateBundleInSelf() {
140+
try {
141+
return LocateBundle(GetSelfPath());
142+
} catch (...) {
143+
return std::nullopt;
144+
}
145+
}
146+
147+
} // namespace flapi

src/include/bundle_locator.hpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#pragma once
2+
3+
#include <cstdint>
4+
#include <filesystem>
5+
#include <optional>
6+
7+
namespace flapi {
8+
9+
// Location of an appended ZIP archive within a host binary.
10+
//
11+
// `offset` is the file offset of the first ZIP local file header --
12+
// the byte you would seek to when slicing the file for libarchive.
13+
// `size` is the total bytes from `offset` through (and including)
14+
// the End-of-Central-Directory record plus its comment, excluding any
15+
// trailing zero padding that the locator tolerates.
16+
struct BundleLocation {
17+
std::uint64_t offset = 0;
18+
std::uint64_t size = 0;
19+
};
20+
21+
// Reverse-scans the file at `path` for a ZIP End-of-Central-Directory
22+
// record. Returns the bundle's location, or nullopt if no valid
23+
// bundle is detected.
24+
//
25+
// Tolerates trailing zero padding after the EOCD record -- the
26+
// spike-caught case of libarchive's default 10240-byte tar-block
27+
// rounding pushing the EOCD off file-EOF.
28+
std::optional<BundleLocation> LocateBundle(const std::filesystem::path& path);
29+
30+
// Convenience: scan the currently running executable. Returns nullopt
31+
// if either the self-path lookup or the EOCD scan fails.
32+
std::optional<BundleLocation> LocateBundleInSelf();
33+
34+
} // namespace flapi

src/include/selfpath.hpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#pragma once
2+
3+
#include <filesystem>
4+
5+
namespace flapi {
6+
7+
// Returns the absolute path of the currently running executable.
8+
// Implemented per-OS:
9+
// - Linux: readlink("/proc/self/exe")
10+
// - macOS: _NSGetExecutablePath
11+
// - Windows: GetModuleFileNameW
12+
// Throws std::system_error on resolution failure.
13+
std::filesystem::path GetSelfPath();
14+
15+
} // namespace flapi

src/selfpath.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#include "selfpath.hpp"
2+
3+
#include <cerrno>
4+
#include <string>
5+
#include <system_error>
6+
#include <vector>
7+
8+
#ifdef _WIN32
9+
#define WIN32_LEAN_AND_MEAN
10+
#include <windows.h>
11+
#elif defined(__APPLE__)
12+
#include <mach-o/dyld.h>
13+
#include <cstdint>
14+
#else
15+
#include <unistd.h>
16+
#endif
17+
18+
namespace flapi {
19+
20+
std::filesystem::path GetSelfPath() {
21+
#ifdef _WIN32
22+
std::vector<wchar_t> buf(MAX_PATH);
23+
while (true) {
24+
const DWORD n = GetModuleFileNameW(nullptr, buf.data(), static_cast<DWORD>(buf.size()));
25+
if (n == 0) {
26+
throw std::system_error(static_cast<int>(GetLastError()), std::system_category(),
27+
"GetModuleFileNameW failed");
28+
}
29+
if (n < buf.size()) {
30+
return std::filesystem::path(std::wstring(buf.data(), n));
31+
}
32+
// Buffer too small (MS docs say n == buf.size() means truncated on
33+
// older Windows; on newer it sets ERROR_INSUFFICIENT_BUFFER).
34+
buf.resize(buf.size() * 2);
35+
}
36+
#elif defined(__APPLE__)
37+
std::uint32_t size = 0;
38+
// First call sizes the buffer.
39+
_NSGetExecutablePath(nullptr, &size);
40+
std::vector<char> buf(size);
41+
if (_NSGetExecutablePath(buf.data(), &size) != 0) {
42+
throw std::system_error(errno, std::generic_category(),
43+
"_NSGetExecutablePath failed");
44+
}
45+
std::error_code ec;
46+
auto canonical = std::filesystem::canonical(std::filesystem::path(buf.data()), ec);
47+
if (ec) {
48+
return std::filesystem::path(buf.data());
49+
}
50+
return canonical;
51+
#else
52+
std::error_code ec;
53+
auto resolved = std::filesystem::read_symlink("/proc/self/exe", ec);
54+
if (ec) {
55+
throw std::system_error(ec, "read_symlink(/proc/self/exe) failed");
56+
}
57+
return resolved;
58+
#endif
59+
}
60+
61+
} // namespace flapi

test/cpp/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ add_executable(flapi_tests
44
main.cpp
55
archive_io_test.cpp
66
audit_logger_test.cpp
7+
bundle_locator_test.cpp
78
auth_middleware_test.cpp
89
config_manager_test.cpp
910
config_manager_yaml_validation_test.cpp

0 commit comments

Comments
 (0)