Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ echo "look" | ./build/archetype --source=games/gorreven.arch --include=games

- `include "file"` in Archetype source uses implicit `.arch` extension; do not include the extension explicitly.
- Compiled `.acx` files are fully resumable — a save file is a mutated copy of the original binary.
- An `.acx` begins with the four bytes `7F 41 43 58` and a format version. The version describes the byte layout only; it is not the interpreter's version, and it must not move when a release changes no layout. Files written before the header still load.
- The `--include=games` flag is needed when compiling from source so the compiler can find `standard.arch` and other shared library files.
15 changes: 15 additions & 0 deletions src/FileStorage.cc
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ namespace archetype {
void InFileStorage::write(span<const Byte>) {
}

int InFileStorage::peek(span<Byte> buf) {
int bytes_read = read(buf);
// A short read at the end of the file sets eofbit, and seekg does
// nothing at all while a stream is in a failed state, so the flags have
// to come off before the position can be put back.
stream_.clear();
stream_.seekg(-bytes_read, ios::cur);
remaining_ += bytes_read;
return bytes_read;
}

OutFileStorage::OutFileStorage(const filesystem::path& filename):
failed_{false}
{
Expand Down Expand Up @@ -78,6 +89,10 @@ namespace archetype {
return 0;
}

int OutFileStorage::peek(span<Byte>) {
return 0;
}

void OutFileStorage::write(span<const Byte> buf) {
stream_.write(reinterpret_cast<const char*>(buf.data()), static_cast<streamsize>(buf.size()));
if (not stream_) {
Expand Down
2 changes: 2 additions & 0 deletions src/FileStorage.hh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ namespace archetype {
virtual int remaining() const override;
virtual int read(std::span<Byte> buf) override;
virtual void write(std::span<const Byte> buf) override;
virtual int peek(std::span<Byte> buf) override;
private:
std::ifstream stream_;
int remaining_;
Expand All @@ -53,6 +54,7 @@ namespace archetype {
virtual int remaining() const override;
virtual int read(std::span<Byte> buf) override;
virtual void write(std::span<const Byte> buf) override;
virtual int peek(std::span<Byte> buf) override;
private:
std::ofstream stream_;
bool failed_;
Expand Down
34 changes: 31 additions & 3 deletions src/IdIndex.hh
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@

#include <iostream>
#include <concepts>
#include <format>
#include <functional>
#include <map>
#include <deque>
#include <set>
#include <stdexcept>
#include <vector>
#include <cassert>

Expand Down Expand Up @@ -153,9 +155,26 @@ namespace archetype {
}

void read(Storage& in) {
int total_entries;
int indexed_entries;
in >> total_entries >> indexed_entries;
// Both of these arrive from the file, and both used to be believed
// on sight. A registry cannot hold more records than the stream
// has bytes to encode them in, and cannot hold more records than it
// has slots.
//
// Bounding the slot count by the bytes remaining is the one place
// this is stricter than the format strictly requires: a free slot
// costs nothing to write, so a registry whose holes outnumbered the
// whole file would now be refused. Free slots are reused rather
// than accumulated, so that registry would have to come from a game
// that had once held more objects than its save has bytes -- and if
// one ever does, the answer is to write the holes down rather than
// to stop checking.
int total_entries = readCount(in, "registry size");
int indexed_entries = readCount(in, "registry record count");
if (indexed_entries > total_entries) {
throw std::invalid_argument(
std::format("Registry of {} slots cannot hold {} records",
total_entries, indexed_entries));
}
registry_.resize(total_entries, T{});
// Nothing about the holes is written down, and nothing needs to be.
// Each record names the slot it belongs in, so the occupied slots
Expand All @@ -171,6 +190,15 @@ namespace archetype {
for (int ii = 0; ii < indexed_entries; ++ii) {
int value_index;
in >> value_index;
// A record naming a slot the registry does not have was an
// out-of-bounds write straight into the deque, reachable from
// any file at all: thirteen crafted bytes segfaulted the
// interpreter here.
if (value_index < 0 or value_index >= total_entries) {
throw std::invalid_argument(
std::format("Record names slot {} in a registry of {}",
value_index, total_entries));
}
in >> registry_[value_index];
index_[registry_[value_index]] = value_index;
occupied[value_index] = true;
Expand Down
45 changes: 43 additions & 2 deletions src/Serialization.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
// For Windows
#define _SCL_SECURE_NO_WARNINGS

#include <array>
#include <span>
#include <stdexcept>
#include <algorithm>
Expand Down Expand Up @@ -96,8 +97,7 @@ namespace archetype {
}

Storage& operator>>(Storage& in, std::string& value) {
int size;
in >> size;
int size = readCount(in, "string length");
value.resize(size);
int bytes_read = in.read({reinterpret_cast<Storage::Byte*>(value.data()), value.size()});
if (bytes_read != size) {
Expand All @@ -108,6 +108,41 @@ namespace archetype {
return in;
}

int readCount(Storage& in, string_view what) {
int count = in.readInteger();
if (count < 0) {
throw invalid_argument(format("A {} cannot be negative: {}", what, count));
}
if (count > in.remaining()) {
throw invalid_argument(
format("A {} of {} is more than the {} bytes remaining can supply",
what, count, in.remaining()));
}
return count;
}

void writeFormatHeader(Storage& out) {
out.write(FormatCookie);
out << CurrentFormatVersion;
}

int readFormatHeader(Storage& in) {
array<Storage::Byte, FormatCookie.size()> cookie{};
if (in.peek(cookie) != static_cast<int>(cookie.size()) or
not ranges::equal(cookie, FormatCookie)) {
return UnversionedFormat;
}
in.read(cookie);
int version = in.readInteger();
if (version <= UnversionedFormat or version > CurrentFormatVersion) {
throw invalid_argument(
format("Archetype format version {} is not one this interpreter "
"understands; it reads up to version {}",
version, CurrentFormatVersion));
}
return version;
}

MemoryStorage::MemoryStorage():
seekIndex_{0}
{ }
Expand All @@ -128,4 +163,10 @@ namespace archetype {
ranges::copy(buf, back_inserter(bytes_));
}

int MemoryStorage::peek(span<Byte> buf) {
int bytes_read = read(buf);
seekIndex_ -= bytes_read;
return bytes_read;
}

}
49 changes: 49 additions & 0 deletions src/Serialization.hh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#ifndef __archetype__Serialization__
#define __archetype__Serialization__

#include <array>
#include <iostream>
#include <span>
#include <string>
Expand All @@ -28,6 +29,12 @@ namespace archetype {
virtual int read(std::span<Byte> buf) = 0;
virtual void write(std::span<const Byte> buf) = 0;

// Look at what read would return, without consuming it. Deciding
// whether a stream carries a format header means examining its first
// bytes before knowing whether they belong to the header or to the
// universe, and only a look that can be taken back can do that.
virtual int peek(std::span<Byte> buf) = 0;

int readInteger();
void writeInteger(int value);
};
Expand All @@ -40,6 +47,47 @@ namespace archetype {
Storage& operator<<(Storage& out, std::string_view value);
Storage& operator>>(Storage& in, std::string& value);

// The four bytes an Archetype binary begins with. 0x7F is neither
// printable nor a legal start to a UTF-8 sequence, so a text file mistaken
// for a game is rejected on its first byte.
inline constexpr std::array<Storage::Byte, 4> FormatCookie{0x7F, 'A', 'C', 'X'};

// The version of the *layout* that follows the cookie, which is a separate
// thing from the interpreter's own version and deliberately so: a release
// that moves no bytes around must leave every .acx exactly as it was, or
// the goldens churn and byte comparison stops being an oracle. Bump this
// only when what operator<< writes actually changes shape.
inline constexpr int CurrentFormatVersion = 1;

// Version 0 is the unversioned layout that predates the header, still
// written by no one and still readable by everyone.
inline constexpr int UnversionedFormat = 0;

// Read a number that counts something the stream still has to supply.
//
// Such a count is a promise about bytes that follow, and no stream can make
// good on a promise bigger than what is left in it: every element, however
// small, costs at least one byte to encode. Checking that before believing
// the number is the whole point -- believing it first is how a file of
// thirteen bytes used to ask for a vector of fifty million.
//
// "what" names the thing being counted, for the message thrown if the count
// is negative or larger than the stream can back.
int readCount(Storage& in, std::string_view what);

void writeFormatHeader(Storage& out);

// The format version the stream declares, with the header consumed if there
// was one. A stream that does not begin with the cookie is left untouched
// and reported as UnversionedFormat; deciding whether that is good enough
// is the caller's business, since only the caller knows what the
// unversioned layout was supposed to start with.
//
// Throws if the cookie is there but the version is one this interpreter
// does not know, which is the case worth a precise complaint: the file is
// definitely ours and definitely from the future.
int readFormatHeader(Storage& in);

class MemoryStorage : public Storage {
size_t seekIndex_;
std::vector<Byte> bytes_;
Expand All @@ -50,6 +98,7 @@ namespace archetype {
virtual int remaining() const override;
virtual int read(std::span<Byte> buf) override;
virtual void write(std::span<const Byte> buf) override;
virtual int peek(std::span<Byte> buf) override;
};
}

Expand Down
Loading
Loading