fix: three errors that ended with the wrong exit code

Found compiling the environment-variable table for AGENTS.md:
- A REVIEW_GATES_MEM_RESERVE_MB that is no number threw
  std::invalid_argument, which nothing caught: the process aborted where
  it should end with a ValueError, exit 1. nan and inf were an unchecked
  cast. They raise ValueError / OverflowError now; a value past int64
  saturates.
- An env file that is not UTF-8: `run` ended with exit 1 where its
  preflight should stop it (exit 3), and `seats` parsed the raw bytes. Both
  read it as strict UTF-8 text now.
- A broker address with a port past 65535 was cut to 16 bits; it is an
  OverflowError now, uncaught (exit 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 16:35:26 +01:00
commit 53b7d6de31
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
9 changed files with 58 additions and 14 deletions

View file

@ -515,9 +515,9 @@ inventory unit, against a frozen commit range.)";
if (envFile.empty() && fs::exists(".env", error)) envFile = ".env";
std::map<std::string, std::string> env = process;
if (!envFile.empty()) {
const std::optional<std::string> text = Platform::ReadFile(envFile);
if (!text) throw Dyn::Error("FileNotFoundError", std::format("[Errno 2] No such file or directory: {}", Dyn::Repr(envFile)));
for (const auto& [name, value] : Seats::SeatTokensFromEnvFile(*text)) env[name] = value;
// The env file, read as text: one that cannot be read, or is not
// UTF-8, ends the command.
for (const auto& [name, value] : Seats::SeatTokensFromEnvFile(Dyn::ReadText(envFile))) env[name] = value;
}
std::vector<SeatJob> jobs;
for (const auto& [seat, token] : Seats::SeatTokensFromEnv(env)) jobs.push_back({seat, token, false});

View file

@ -20,8 +20,15 @@ namespace Crafter::Review::Memory {
std::int64_t Reserve(const std::optional<std::string>& configured, const fs::path& path) {
if (configured && !configured->empty()) {
// A number, truncated toward zero; ValueError or OverflowError for
// what is none. A reserve past int64 saturates: it holds every MB
// either way.
const std::optional<double> value = Dyn::ParseFloat(*configured);
if (!value) throw std::invalid_argument(Dyn::FloatError(*configured));
if (!value) throw Dyn::Error("ValueError", Dyn::FloatError(*configured));
if (std::isnan(*value)) throw Dyn::Error("ValueError", "cannot convert float NaN to integer");
if (std::isinf(*value)) throw Dyn::Error("OverflowError", "cannot convert float infinity to integer");
if (*value >= 0x1p63) return std::numeric_limits<std::int64_t>::max();
if (*value < -0x1p63) return std::numeric_limits<std::int64_t>::min();
return static_cast<std::int64_t>(*value);
}
const std::map<std::string, std::int64_t> info = Platform::MemInfo(path);

View file

@ -1120,14 +1120,17 @@ namespace Crafter::Review::PgWire {
const std::size_t colon = where.rfind(':');
const std::string host(colon == std::string_view::npos ? std::string_view() : where.substr(0, colon));
const std::string portText(colon == std::string_view::npos ? where : where.substr(colon + 1));
const std::optional<std::int64_t> port = Dyn::ParseInt(portText);
if (!port) {
out(std::format("{} - the driver's SQL broker does not answer ({})", NoDatabase, Dyn::IntError(portText)));
return ExitNoDb;
}
// socket.create_connection raised OverflowError for it, which
// call_broker did not catch: the helper ended in a traceback.
if (*port < 0 || *port > 65535) throw Dyn::Error("OverflowError", "getsockaddrarg: port must be 0-65535.");
std::unique_ptr<ClientTCP> socket;
try {
const std::optional<std::int64_t> port = Dyn::ParseInt(portText);
if (!port) throw std::invalid_argument(Dyn::IntError(portText));
socket = std::make_unique<ClientTCP>(host, static_cast<std::uint16_t>(*port), std::chrono::seconds(5));
} catch (const std::invalid_argument& error) {
out(std::format("{} - the driver's SQL broker does not answer ({})", NoDatabase, error.what()));
return ExitNoDb;
} catch (const std::exception& error) {
out(std::format("{} - the driver's SQL broker does not answer ({})", NoDatabase, ConnectErrorText(error)));
return ExitNoDb;

View file

@ -620,7 +620,10 @@ namespace Crafter::Review::Run {
} catch (const std::invalid_argument& e) {
Die(e.what());
} catch (const Dyn::Error& e) {
if (e.Kind() != "FileNotFoundError" && e.Kind() != "PermissionError" && e.Kind() != "IsADirectoryError" && e.Kind() != "OSError") throw;
// The errors that mean the file could not be read: the OS errors, a
// file that is not UTF-8 (UnicodeDecodeError), and ValueError.
constexpr std::array<std::string_view, 6> Caught = {"FileNotFoundError", "PermissionError", "IsADirectoryError", "OSError", "UnicodeDecodeError", "ValueError"};
if (!std::ranges::contains(Caught, e.Kind())) throw;
Die(e.what());
}
// The board (2026-10-31): the other drivers on this machine, what they