// sample_fs.cpp — host-side virtual sample filesystem. // // This module parses .mfs files and edits the image-relative QSPI backing // store. It does not know about Main, Unicorn, or the memory-mapped QSPI // window. #include "storage/sample_fs.hpp" #include "storage/qspi_image.hpp" #include #include #include #include #include #include #include #include namespace mf::storage::sample_fs { namespace { constexpr uint32_t kFsBase = 0x00281000U; constexpr uint32_t kFsEnd = 0x00F51000U; constexpr uint32_t kSampleMax = 0x00177000U; constexpr uint32_t kRecordHeader = 0x18U; struct MfsSample { std::string name; std::vector pcm; }; // Cursor over the Boost text archive. Strings are length-prefixed and // followed by exactly one separator space (names may contain spaces), while // integers are whitespace-separated. class TextScanner { public: explicit TextScanner(const std::span bytes) : p_(reinterpret_cast(bytes.data())), end_(p_ + bytes.size()) {} bool read_i64(int64_t &v) { while (p_ < end_ && (*p_ == ' ' || *p_ == '\n' || *p_ == '\r' || *p_ == '\t')) { p_++; } bool neg = false; if (p_ < end_ && (*p_ == '-' || *p_ == '+')) { neg = *p_ == '-'; p_++; } if (p_ >= end_ || *p_ < '0' || *p_ > '9') { return false; } int64_t acc = 0; while (p_ < end_ && *p_ >= '0' && *p_ <= '9') { if (acc > (std::numeric_limits::max() - 9) / 10) { return false; } acc = acc * 10 + (*p_ - '0'); p_++; } v = neg ? -acc : acc; return true; } bool read_u64(uint64_t &v) { int64_t i = 0; if (!read_i64(i) || i < 0) { return false; } v = static_cast(i); return true; } bool skip(const size_t n) { if (static_cast(end_ - p_) < n) { return false; } p_ += n; return true; } bool read_raw(char *dst, const size_t n) { if (static_cast(end_ - p_) < n) { return false; } std::memcpy(dst, p_, n); p_ += n; return true; } private: const char *p_; const char *end_; }; // Parse a `.mfs` (factory sample or Elektroid-exported one). bool parse_mfs(const std::span bytes, MfsSample &out, std::string &why) { TextScanner s(bytes); uint64_t tag_len = 0; std::array tag{}; if (!s.read_u64(tag_len) || tag_len + 1 > tag.size() || !s.skip(1) || !s.read_raw(tag.data(), tag_len) || std::string_view(tag.data(), tag_len) != "serialization::archive") { why = "not a Boost text archive"; return false; } int64_t major = 0; int64_t zero1 = 0; int64_t four = 0; uint64_t ver_len = 0; if (!s.read_i64(major) || !s.read_i64(zero1) || !s.read_i64(four) || !s.read_u64(ver_len) || major != 10 || zero1 != 0 || four != 4 || !s.skip(1) || !s.skip(ver_len)) { why = "bad archive prologue"; return false; } uint64_t name_len = 0; if (!s.read_u64(name_len) || name_len == 0 || name_len > 255 || !s.skip(1)) { why = "bad name length"; return false; } out.name.assign(name_len, '\0'); if (!s.read_raw(out.name.data(), name_len)) { why = "truncated name"; return false; } int64_t p0 = 0; int64_t z2 = 0; int64_t z3 = 0; uint64_t zeros_len = 0; if (!s.read_i64(p0) || !s.read_i64(z2) || !s.read_i64(z3) || !s.read_u64(zeros_len) || zeros_len != 18 || !s.skip(1) || !s.skip(zeros_len)) { why = "bad sample header"; return false; } int64_t p3 = 0; int64_t z4 = 0; int64_t p5 = 0; uint64_t datalen = 0; if (!s.read_i64(p3) || !s.read_i64(z4) || !s.read_i64(p5) || !s.read_u64(datalen)) { why = "missing data length"; return false; } if (datalen == 0) { why = "metadata stub (no sample data)"; return false; } if (datalen > kSampleMax) { why = "sample larger than 0x177000 bytes"; return false; } out.pcm.resize(datalen); for (size_t i = 0; i < datalen; i++) { int64_t v = 0; if (!s.read_i64(v)) { why = "truncated sample data"; return false; } out.pcm[i] = static_cast(v & 0xFF); } return true; } uint32_t rd_le32(const std::byte *p) { return static_cast(std::to_integer(p[0])) | (static_cast(std::to_integer(p[1])) << 8) | (static_cast(std::to_integer(p[2])) << 16) | (static_cast(std::to_integer(p[3])) << 24); } void wr_le16(std::byte *p, const uint16_t v) { p[0] = std::byte(v & 0xFF); p[1] = std::byte((v >> 8) & 0xFF); } void wr_le32(std::byte *p, const uint32_t v) { p[0] = std::byte(v & 0xFF); p[1] = std::byte((v >> 8) & 0xFF); p[2] = std::byte((v >> 16) & 0xFF); p[3] = std::byte((v >> 24) & 0xFF); } } // namespace InstallResult install(const std::span image, const std::span mfs, const uint32_t slot) { if (image.size() != qspi::kSize) { throw Error{Status::argument, "invalid QSPI image"}; } if (slot > 127) { throw Error{Status::argument, "sample slot out of range (0..127)"}; } MfsSample sample; std::string why; if (!parse_mfs(mfs, sample, why)) { throw Error{Status::argument, ".mfs: " + why}; } const uint32_t payload = static_cast(sample.pcm.size()); uint32_t cksum = 0; for (size_t i = 0; i + 1 < sample.pcm.size(); i += 2) { cksum += static_cast(sample.pcm[i] | (sample.pcm[i + 1] << 8)); } std::array rec_name{}; const size_t name_len = std::min(sample.name.size(), 12); std::memcpy(rec_name.data(), sample.name.data(), name_len); // Best-fit allocation over the record gaps, mirroring FUN_08045f50. const uint32_t rec_len = kRecordHeader + payload; uint32_t target = 0; uint32_t best_gap = UINT32_MAX; uint32_t gap_start = kFsBase; for (uint32_t cursor = kFsBase; cursor + kRecordHeader <= kFsEnd;) { const uint32_t rec_addr = rd_le32(image.data() + cursor); const uint32_t rec_size = rd_le32(image.data() + cursor + 4); const uint8_t rec_id = std::to_integer(image[cursor + 0x17]); if (rec_addr != cursor) { cursor += 0x1000U; continue; } if (rec_id == 0xFF) { cursor += (rec_size & ~0xFFFU) + 0x1000U; continue; } if (rec_id == slot) { if (rec_size == payload && static_cast(rd_le32(image.data() + cursor + 8) & 0xFFFF) == static_cast(cksum) && std::memcmp(image.data() + cursor + 0x0A, rec_name.data(), rec_name.size()) == 0) { return InstallResult{sample.name, cursor, payload, true}; } throw Error{Status::argument, "sample slot occupied by a different sample"}; } const uint32_t gap = cursor - gap_start; if (gap >= rec_len && gap < best_gap) { best_gap = gap; target = gap_start; } cursor += (rec_size & ~0xFFFU) + 0x1000U; gap_start = cursor; } const uint32_t tail = kFsEnd - gap_start; if (tail >= rec_len && tail < best_gap) { target = gap_start; } if (target == 0) { throw Error{Status::argument, "sample file system full"}; } std::array hdr{}; wr_le32(hdr.data() + 0, target); wr_le32(hdr.data() + 4, payload); wr_le16(hdr.data() + 8, static_cast(cksum)); std::memcpy(hdr.data() + 0x0A, rec_name.data(), rec_name.size()); hdr[0x17] = std::byte(slot); std::memset(image.data() + target, 0, kRecordHeader + payload); std::memcpy(image.data() + target, hdr.data(), hdr.size()); std::memcpy(image.data() + target + kRecordHeader, sample.pcm.data(), payload); return InstallResult{sample.name, target, payload, false}; } } // namespace mf::storage::sample_fs