// vdm/meta/veloxpart.cpp // // Reader first (AGENT-CORE §5). parse_veloxpart() is the attacker-facing surface; it is // total on any byte string. #include "vdm/meta/veloxpart.hpp" #include #include #include #include #include #include #include "vdm/util/crc32.hpp" namespace vdm::meta { namespace { // magic(4)+ver(2)+flags(2)+total(8)+downloaded(8)+url_count(4) // +etag_len(4)+lm_len(4)+ct_len(4)+seg_count(4)+crc(4) constexpr std::size_t kMinImageBytes = 52; constexpr char kMagic[4] = {'V', 'D', 'M', 'P'}; Error errno_to_error(int e) noexcept { switch (e) { case ENOSPC: case EDQUOT: return Error::disk_full; case EACCES: case EPERM: case EROFS: return Error::permission_denied; case ENOENT: case ENOTDIR: case EISDIR: case ENAMETOOLONG: case ELOOP: return Error::path_rejected; default: return Error::io_error; } } ErrorInfo sys_error(std::string_view what, int e) { return ErrorInfo(errno_to_error(e), std::string(what) + ": " + std::strerror(e)); } ErrorInfo corrupt(std::string_view where) { return ErrorInfo(Error::meta_corrupt, std::string(".veloxpart.meta: ") + std::string(where)); } } // namespace // --------------------------------------------------------------------------------------- // Reader Result parse_veloxpart(ConstByteSpan image) { if (image.size() > kMaxImageBytes) return corrupt("image exceeds cap"); if (image.size() < kMinImageBytes) return corrupt("image shorter than the header"); // CRC over everything but the trailing u32 — reject before interpreting any field. const ConstByteSpan body = image.first(image.size() - 4); const std::uint32_t want = load_le(image.subspan(image.size() - 4)); if (crc32(body) != want) return corrupt("crc32 mismatch"); ByteReader r(body); if (as_chars(r.raw(4)) != std::string_view(kMagic, 4)) return corrupt("bad magic"); VeloxPart vp; vp.version = r.u16(); vp.flags = r.u16(); if (vp.version > kVersion) return ErrorInfo(Error::meta_version_unsupported, ".veloxpart.meta: version " + std::to_string(vp.version) + " > supported " + std::to_string(kVersion)); vp.total_size = r.u64(); vp.downloaded = r.u64(); const std::uint32_t url_count = r.u32(); if (url_count > kMaxUrls) return corrupt("url_count past cap"); if (static_cast(url_count) * 4 > r.remaining()) return corrupt("url_count"); vp.urls.reserve(url_count); for (std::uint32_t i = 0; i < url_count; ++i) { std::string_view s = r.lp_string(); if (r.overran() || s.size() > kMaxStringLen) return corrupt("url"); vp.urls.emplace_back(s); } auto read_str = [&](std::string &dst, std::string_view what) -> Result { std::string_view s = r.lp_string(); if (r.overran() || s.size() > kMaxStringLen) return corrupt(what); dst.assign(s); return ok(); }; VDM_TRY(read_str(vp.etag, "etag")); VDM_TRY(read_str(vp.last_modified, "last_modified")); VDM_TRY(read_str(vp.content_type, "content_type")); const std::uint32_t seg_count = r.u32(); if (seg_count > kMaxSegments) return corrupt("segment_count past cap"); if (static_cast(seg_count) * 24 > r.remaining()) return corrupt("segment_count"); vp.segments.reserve(seg_count); for (std::uint32_t i = 0; i < seg_count; ++i) { SegmentRecord s; s.start = r.u64(); s.end = r.u64(); s.completed = r.u64(); if (r.overran()) return corrupt("segment record"); if (s.end >= s.start && s.completed > s.end - s.start + 1) return corrupt("segment.completed exceeds its range"); vp.segments.push_back(s); } if (vp.flags & kFlagHasShaState) { const std::uint32_t n = r.u32(); if (r.overran() || n > kMaxShaStateLen) return corrupt("sha256_state length"); ConstByteSpan blob = r.raw(n); if (r.overran()) return corrupt("sha256_state body"); vp.sha256_state.assign(blob.begin(), blob.end()); } if (r.overran()) return corrupt("truncated"); if (r.remaining() != 0) return corrupt("trailing bytes after the record"); return vp; } // --------------------------------------------------------------------------------------- // Writer std::vector serialize_veloxpart(const VeloxPart &vp) { std::vector out; std::size_t est = kMinImageBytes + vp.segments.size() * 24 + vp.sha256_state.size() + 64; for (const auto &u : vp.urls) est += 4 + u.size(); est += vp.etag.size() + vp.last_modified.size() + vp.content_type.size(); out.reserve(est); auto put_bytes = [&](const void *p, std::size_t n) { const auto *b = static_cast(p); out.insert(out.end(), b, b + n); }; auto put_u16 = [&](std::uint16_t v) { std::byte t[2]; store_le(t, v); put_bytes(t, 2); }; auto put_u32 = [&](std::uint32_t v) { std::byte t[4]; store_le(t, v); put_bytes(t, 4); }; auto put_u64 = [&](std::uint64_t v) { std::byte t[8]; store_le(t, v); put_bytes(t, 8); }; auto put_str = [&](std::string_view s) { put_u32(static_cast(s.size())); put_bytes(s.data(), s.size()); }; // Normalise the sha-state flag to match the payload so a round-trip is exact. std::uint16_t flags = vp.flags; if (vp.sha256_state.empty()) flags &= static_cast(~kFlagHasShaState); else flags |= kFlagHasShaState; put_bytes(kMagic, 4); put_u16(vp.version); put_u16(flags); put_u64(vp.total_size); put_u64(vp.downloaded); put_u32(static_cast(vp.urls.size())); for (const auto &u : vp.urls) put_str(u); put_str(vp.etag); put_str(vp.last_modified); put_str(vp.content_type); put_u32(static_cast(vp.segments.size())); for (const auto &s : vp.segments) { put_u64(s.start); put_u64(s.end); put_u64(s.completed); } if (!vp.sha256_state.empty()) { put_u32(static_cast(vp.sha256_state.size())); put_bytes(vp.sha256_state.data(), vp.sha256_state.size()); } put_u32(crc32(ConstByteSpan(out.data(), out.size()))); return out; } // --------------------------------------------------------------------------------------- // File helpers Result read_veloxpart_file(std::string_view path) { std::string p(path); int fd = ::open(p.c_str(), O_RDONLY | O_CLOEXEC); if (fd < 0) return sys_error("open " + p, errno); std::vector buf; buf.resize(kMaxImageBytes + 1); std::size_t total = 0; for (;;) { ssize_t n = ::read(fd, buf.data() + total, buf.size() - total); if (n < 0) { if (errno == EINTR) continue; int e = errno; ::close(fd); return sys_error("read " + p, e); } if (n == 0) break; total += static_cast(n); if (total > kMaxImageBytes) { ::close(fd); return corrupt("sidecar file exceeds cap"); } } ::close(fd); buf.resize(total); return parse_veloxpart(ConstByteSpan(buf.data(), buf.size())); } Result write_veloxpart_file(std::string_view path, const VeloxPart &vp, bool fsync) { std::string p(path); std::string tmp = p + ".tmp"; std::vector image = serialize_veloxpart(vp); int fd = ::open(tmp.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); if (fd < 0) return sys_error("open " + tmp, errno); const std::byte *pd = image.data(); std::size_t remaining = image.size(); while (remaining > 0) { ssize_t n = ::write(fd, pd, remaining); if (n < 0) { if (errno == EINTR) continue; int e = errno; ::close(fd); ::unlink(tmp.c_str()); return sys_error("write " + tmp, e); } pd += n; remaining -= static_cast(n); } if (fsync) { while (::fdatasync(fd) != 0) { if (errno == EINTR) continue; int e = errno; ::close(fd); ::unlink(tmp.c_str()); return sys_error("fdatasync " + tmp, e); } } if (::close(fd) != 0) { int e = errno; ::unlink(tmp.c_str()); return sys_error("close " + tmp, e); } if (::rename(tmp.c_str(), p.c_str()) != 0) { int e = errno; ::unlink(tmp.c_str()); return sys_error("rename " + tmp + " -> " + p, e); } if (fsync) { // fsync the directory so the rename itself is durable. std::string dir = p.substr(0, p.find_last_of('/')); if (dir.empty() || dir == p) dir = "."; int dfd = ::open(dir.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC); if (dfd >= 0) { while (::fsync(dfd) != 0 && errno == EINTR) { } ::close(dfd); } } return ok(); } } // namespace vdm::meta