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
32 changes: 31 additions & 1 deletion apps/qsl-client/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,43 @@

#include <arpa/inet.h>
#include <array>
#include <charconv>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <limits>
#include <netinet/in.h>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <sys/socket.h>
#include <unistd.h>
#include <vector>

namespace {

// Parse a whole token as a TCP port (0-65535). Rejects junk, trailing characters, and out-of-range
// values instead of std::stoul's throw-or-silently-truncate behavior.
std::optional<std::uint16_t> parse_port(std::string_view s) {
std::uint32_t value = 0;
const char *begin = s.data();
const char *end = begin + s.size();
const auto [ptr, ec] = std::from_chars(begin, end, value);
if (ec != std::errc{} || ptr != end || value > std::numeric_limits<std::uint16_t>::max()) {
return std::nullopt;
}
return static_cast<std::uint16_t>(value);
}

// The optional port arg defaults to 9009; nullopt means it was present but malformed.
std::optional<std::uint16_t> port_from_args(int argc, char **argv) {
if (argc < 2) {
return std::uint16_t{9009};
}
return parse_port(argv[1]);
}

bool write_all(int fd, const std::vector<std::byte> &data) {
std::size_t offset = 0;
while (offset < data.size()) {
Expand Down Expand Up @@ -73,7 +98,12 @@ void print_responses(std::span<const std::byte> bytes) {

// qsl-client [port] -> connect to 127.0.0.1:port, send a NewOrder and a Heartbeat, print replies.
int main(int argc, char **argv) {
const std::uint16_t port = (argc >= 2) ? static_cast<std::uint16_t>(std::stoul(argv[1])) : 9009;
const auto port_opt = port_from_args(argc, argv);
if (!port_opt) {
std::cerr << "usage: qsl-client [port] (one optional port, 0-65535)\n";
return 2;
}
const std::uint16_t port = *port_opt;

const int fd = ::socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
Expand Down
40 changes: 38 additions & 2 deletions apps/qsl-export-fixture/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,48 @@
#include "qsl/replay/dispatch.hpp"
#include "qsl/replay/recovery.hpp"

#include <charconv>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <optional>
#include <ostream>
#include <string_view>
#include <type_traits>
#include <variant>

using namespace qsl;

namespace {

// Parse a whole token as an unsigned integer; rejects junk/trailing chars/overflow rather than
// std::stoull's throw-on-bad-input (which would call std::terminate from main).
std::optional<std::uint64_t> parse_u64(std::string_view s) {
std::uint64_t value = 0;
const char *begin = s.data();
const char *end = begin + s.size();
const auto [ptr, ec] = std::from_chars(begin, end, value);
if (ec != std::errc{} || ptr != end) {
return std::nullopt;
}
return value;
}

struct FixtureArgs {
std::uint64_t seed;
std::size_t orders;
};

// Optional [seed] [orders] args (defaults 42 / 200); nullopt if either is present but malformed.
std::optional<FixtureArgs> parse_args(int argc, char **argv) {
const auto seed = (argc >= 2) ? parse_u64(argv[1]) : std::optional<std::uint64_t>{42};
const auto orders = (argc >= 3) ? parse_u64(argv[2]) : std::optional<std::uint64_t>{200};
if (!seed || !orders) {
return std::nullopt;
}
return FixtureArgs{*seed, static_cast<std::size_t>(*orders)};
}

// Emit one normalized line per engine event (in emission order).
void emit_events(std::ostream &os, const std::vector<engine::EngineEvent> &events) {
for (const auto &event : events) {
Expand Down Expand Up @@ -45,8 +76,13 @@ void emit_events(std::ostream &os, const std::vector<engine::EngineEvent> &event
// max_order_quantity makes some new orders reject, so the fixture exercises the
// "rejected order never rests" invariant. Engine-neutral: no engine code is modified.
int main(int argc, char **argv) {
const std::uint64_t seed = (argc >= 2) ? std::stoull(argv[1]) : 42;
const std::size_t orders = (argc >= 3) ? std::stoull(argv[2]) : 200;
const auto args = parse_args(argc, argv);
if (!args) {
std::cerr << "usage: qsl-export-fixture [seed] [orders]\n";
return 2;
}
const std::uint64_t seed = args->seed;
const std::size_t orders = args->orders;
const core::SymbolId symbols = 4;
const core::Quantity max_qty = 8; // generate_flow can emit qty > 8 -> some orders reject

Expand Down
77 changes: 66 additions & 11 deletions apps/qsl-mdfeed/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,57 @@
#include "qsl/feed/udp_feed.hpp"
#include "qsl/replay/recovery.hpp"

#include <charconv>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
#include <variant>

namespace {

// Parse a whole token as an unsigned integer, rejecting junk/trailing chars/overflow (unlike
// std::stoul/std::stoull which throw on bad input and std::stoi which can wrap a port silently).
std::optional<std::uint64_t> parse_u64(std::string_view s) {
std::uint64_t value = 0;
const char *begin = s.data();
const char *end = begin + s.size();
const auto [ptr, ec] = std::from_chars(begin, end, value);
if (ec != std::errc{} || ptr != end) {
return std::nullopt;
}
return value;
}

std::optional<std::uint16_t> parse_port(std::string_view s) {
const auto value = parse_u64(s);
if (!value || *value > std::numeric_limits<std::uint16_t>::max()) {
return std::nullopt;
}
return static_cast<std::uint16_t>(*value);
}

std::optional<int> parse_rcvbuf(std::string_view s) {
const auto value = parse_u64(s);
if (!value || *value > std::numeric_limits<int>::max()) {
return std::nullopt;
}
return static_cast<int>(*value);
}

template <class... Opts> bool all_present(const Opts &...opts) noexcept {
return (static_cast<bool>(opts) && ...);
}

int usage() {
std::cerr << "usage:\n qsl-mdfeed subscribe <port> [count] [rcvbuf_bytes]\n qsl-mdfeed "
"publish <port> [seed] [orders]\n";
return 2;
}

int publish(std::uint16_t port, std::uint64_t seed, std::size_t orders) {
qsl::engine::MatchingEngine engine;
qsl::feed::MarketDataPublisher publisher;
Expand Down Expand Up @@ -74,22 +117,34 @@ int subscribe(std::uint16_t port, std::size_t count, int recv_buffer_bytes) {

} // namespace

int run_subscribe(int argc, char **argv) {
const auto port = parse_port(argv[2]);
const auto count = (argc >= 4) ? parse_u64(argv[3]) : std::optional<std::uint64_t>{20};
const auto rcvbuf = (argc >= 5) ? parse_rcvbuf(argv[4]) : std::optional<int>{0};
if (!all_present(port, count, rcvbuf)) {
return usage();
}
return subscribe(*port, static_cast<std::size_t>(*count), *rcvbuf);
}

int run_publish(int argc, char **argv) {
const auto port = parse_port(argv[2]);
const auto seed = (argc >= 4) ? parse_u64(argv[3]) : std::optional<std::uint64_t>{42};
const auto orders = (argc >= 5) ? parse_u64(argv[4]) : std::optional<std::uint64_t>{200};
if (!all_present(port, seed, orders)) {
return usage();
}
return publish(*port, *seed, static_cast<std::size_t>(*orders));
}

// qsl-mdfeed subscribe <port> [count] [rcvbuf_bytes] -> receive datagrams and report gaps
// qsl-mdfeed publish <port> [seed] [orders] -> run a synthetic flow, stream its data
int main(int argc, char **argv) {
if (argc >= 3 && std::string(argv[1]) == "subscribe") {
const auto port = static_cast<std::uint16_t>(std::stoul(argv[2]));
const std::size_t count = (argc >= 4) ? std::stoul(argv[3]) : 20;
const int recv_buffer_bytes = (argc >= 5) ? std::stoi(argv[4]) : 0;
return subscribe(port, count, recv_buffer_bytes);
return run_subscribe(argc, argv);
}
if (argc >= 3 && std::string(argv[1]) == "publish") {
const auto port = static_cast<std::uint16_t>(std::stoul(argv[2]));
const std::uint64_t seed = (argc >= 4) ? std::stoull(argv[3]) : 42;
const std::size_t orders = (argc >= 5) ? std::stoul(argv[4]) : 200;
return publish(port, seed, orders);
return run_publish(argc, argv);
}
std::cerr << "usage:\n qsl-mdfeed subscribe <port> [count] [rcvbuf_bytes]\n qsl-mdfeed "
"publish <port> [seed] [orders]\n";
return 2;
return usage();
}
32 changes: 32 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,38 @@ add_test(
-D "EXPECT_OUTPUT=orders must be an unsigned integer" -P
"${CMAKE_CURRENT_LIST_DIR}/cmake/expect_command_failure.cmake")

# CLI argument hardening: malformed numeric args must print usage and exit non-zero, not throw out
# of main (std::terminate) or silently truncate an out-of-range port.
add_test(
NAME qsl_client_invalid_port_fails
COMMAND ${CMAKE_COMMAND} -D "PROGRAM=$<TARGET_FILE:qsl-client>" -D "ARGS=99xyz" -D
"EXPECT_OUTPUT=usage:" -P
"${CMAKE_CURRENT_LIST_DIR}/cmake/expect_command_failure.cmake")

add_test(
NAME qsl_client_out_of_range_port_fails
COMMAND ${CMAKE_COMMAND} -D "PROGRAM=$<TARGET_FILE:qsl-client>" -D "ARGS=70000" -D
"EXPECT_OUTPUT=usage:" -P
"${CMAKE_CURRENT_LIST_DIR}/cmake/expect_command_failure.cmake")

add_test(
NAME qsl_mdfeed_invalid_count_fails
COMMAND
${CMAKE_COMMAND} -D "PROGRAM=$<TARGET_FILE:qsl-mdfeed>" -D "ARGS=subscribe;9009;notanumber" -D
"EXPECT_OUTPUT=usage:" -P "${CMAKE_CURRENT_LIST_DIR}/cmake/expect_command_failure.cmake")

add_test(
NAME qsl_mdfeed_out_of_range_port_fails
COMMAND ${CMAKE_COMMAND} -D "PROGRAM=$<TARGET_FILE:qsl-mdfeed>" -D "ARGS=publish;70000" -D
"EXPECT_OUTPUT=usage:" -P
"${CMAKE_CURRENT_LIST_DIR}/cmake/expect_command_failure.cmake")

add_test(
NAME qsl_export_fixture_invalid_seed_fails
COMMAND ${CMAKE_COMMAND} -D "PROGRAM=$<TARGET_FILE:qsl-export-fixture>" -D "ARGS=abc" -D
"EXPECT_OUTPUT=usage:" -P
"${CMAKE_CURRENT_LIST_DIR}/cmake/expect_command_failure.cmake")

# Shell unit tests for the shared artifact-publish helper (MAC sanitization +
# trailing-whitespace/blank-line trimming). Portable (sed/awk/mktemp); the script
# locates the repo via BASH_SOURCE, so it runs correctly by absolute path.
Expand Down
Loading