fix: ECR17 spec-compliance, build fixes, C++ unit tests + CI#3
Conversation
Verified the implementation against the Nexi ECR17 protocol spec and fixed
correctness/build defects:
- Ecr17Client::status(): non-void function with no return (UB / garbage
struct returned to JS). Now throws a clear "not implemented" error until
the transport send/receive + status-response parsing flow exists.
- Ecr17Protocol.hpp: removed illegal qualified name in the member
declaration (Ecr17Protocol::buildStatusMessage), which fails to compile
on Clang (Android NDK / iOS).
- PacketCodec::decode(): guard the SOH (progress-update) branch against
buffers shorter than 2 bytes (invalid iterator range -> UB/crash); read
the LRC as the byte immediately after ETX instead of data.back() and
require its presence; add missing <algorithm>/<iterator> includes.
- PacketCodec.hpp: fix cross-include "Lcr.hpp" -> "Lcr/Lcr.hpp" (did not
resolve from the ../cpp include root); rename PacketType::STATUS ->
PROGRESS to match the spec (SOH 0x01 frame is a progress update, not the
terminal status response).
- Ecr17Protocol: validate fixed-width fields (throw on overflow) and reject
negative amounts, so a malformed frame is never sent to the terminal.
- Remove Transport/Transport.cpp: it was a byte-for-byte copy of the header
for a pure-abstract interface (no translation unit needed).
- android/CMakeLists.txt: compile the new .cpp sources (Ecr17Client,
Ecr17Protocol, Lcr, PacketCodec); previously only Ecr17.cpp was built, so
HybridEcr17Client symbols would be undefined at link time on Android.
Confirmed correct against the spec (no change needed): status message code
is lowercase 's' (0x73) and the Payment ("P") message layout is exactly 167
bytes with the documented field order/padding.
Also vendored the protocol reference under docs/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ CI Adds a standalone GoogleTest suite (no Nitro/codegen required) that compiles the pure-logic units in isolation and runs on every PR via GitHub Actions. Coverage: - Lrc::compute: base value, all four LrcMode variants, known vectors, string/vector overload parity, and a cross-check against an independent reference implementation. - PacketCodec: application encode framing + round-trip for every mode, corrupted-LRC detection, control (ACK/NAK) framing/decoding, and the edge cases fixed in this branch -- empty buffer, lone SOH (no crash), progress-update frame, STX without ETX, and ETX with no trailing LRC. - Ecr17Protocol: exact 167-byte Payment layout with per-field assertions against the spec, 10-byte status layout with lowercase 's', amount boundary (8 digits), and the new validation (negative amount, oversized fields throw std::invalid_argument). A test-only stubs/LrcMode.hpp mirrors the enum Nitrogen generates so the suite builds without running codegen; it is on the test target's include path only and documents the contract the production code assumes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Implement Ecr17Protocol::buildReversalMessage (command 'S', 26-byte layout per spec; STAN defaults to "000000" = reverse last, no STAN check). - Add test_flows.cpp modelling the documented sequences: basic payment (request -> ACK -> progress -> result -> ACK), NAK retransmission, reversal/"annullamento", pay -> reverse -> repay, and status round-trip. Response field parsing is not implemented yet, so terminal responses are spec-shaped payloads asserted at the framing/classification level. - Add direct unit tests for the reversal layout and STAN validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the bare Nitro template README with a full guide: badges (incl. CI), TOC, an honest feature-status matrix (what works today vs roadmap), junior-proof quick start, Ecr17Config reference, LRC-mode and command-code cheat-sheets, architecture overview, and test instructions. Also fixes the license badge link (pointed at the wrong org). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The repository package folder is `package/` (singular), but biome.json included `packages/**` (plural), so the main package's TypeScript was never linted. Found via the Copilot review report in PR #2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4436cfa2d3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| std::string payload(data.begin() + 1, data.begin() + etxIndex); | ||
|
|
||
| uint8_t rxLrc = data.back(); | ||
| uint8_t rxLrc = data[etxIndex + 1]; |
There was a problem hiding this comment.
Reject trailing bytes after the LRC
When a read buffer contains a complete application frame followed by any extra bytes, this now validates the frame using the byte immediately after ETX and silently ignores the rest. Because DecodedPacket has no field for unconsumed bytes, a coalesced socket read such as STX payload ETX LRC STX ... is reported as one valid APPLICATION packet and the next frame is effectively dropped; similarly, garbage after the LRC is accepted. Please either require etxIndex + 2 == data.size() here or return/preserve the remaining bytes for the transport parser.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 2d8174c. decode() now requires the LRC to be the final byte (etxIndex + 2 == data.size()), so a complete frame followed by extra bytes — a coalesced read with a second frame, or trailing garbage — is rejected as UNKNOWN rather than silently accepted with the remainder dropped. Stream→frame splitting is left to the (roadmap) transport layer. Added regression tests for trailing-bytes and two coalesced frames.
There was a problem hiding this comment.
Pull request overview
Audits and hardens the initial C++ ECR17 protocol implementation against the vendored Nexi specification. Fixes correctness/build defects identified in the prior review, adds a 34-case GoogleTest suite wired into CI, introduces the reversal ('S') builder, and ships a comprehensive package README.
Changes:
- Bug fixes in
PacketCodec(SOH bounds guard, LRC byte position, missing includes,STATUS→PROGRESS),Ecr17Protocol(field-overflow / negative-amount throws),Ecr17Client::status()(throws instead of UB), and the qualified-name compile error inEcr17Protocol.hpp. - New
Ecr17Protocol::buildReversalMessageplus standalone GoogleTest suite (package/cpp/tests/) with a stubLrcMode.hppand a CI workflow. - Build/lint corrections (Android
CMakeLists.txtsource list, Biome globpackages/**→package/**), removal of the duplicateTransport.cppbody, and a full README + vendored spec underdocs/.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
package/cpp/PacketCodec/PacketCodec.{hpp,cpp} |
SOH bounds guard, post-ETX LRC read, PROGRESS rename, include path fix |
package/cpp/Ecr17Protocol/Ecr17Protocol.{hpp,cpp} |
Removes illegal qualified name; adds field/amount validation and reversal builder |
package/cpp/Ecr17Client/Ecr17Client.cpp |
status() now throws "not implemented" instead of falling off non-void |
package/cpp/Transport/Transport.cpp |
Removed (was a duplicate of the abstract header) |
package/cpp/tests/* + CMakeLists.txt + stubs/LrcMode.hpp |
New GoogleTest suite covering Lrc / PacketCodec / Protocol / documented flows |
.github/workflows/cpp-tests.yml |
CI job building and running the C++ test suite on Ubuntu |
package/android/CMakeLists.txt |
Adds the additional .cpp sources to the Android shared library |
biome.json |
Fixes lint glob (packages/** → package/**) |
package/README.md |
Full rewrite with feature-status matrix, configuration reference, and roadmap |
docs/standard ECR17 - supported by Nexi Group terminals.md |
Vendored protocol reference |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…iew) decode() now requires the LRC to be the final byte of an STX application frame (etxIndex + 2 == data.size()), rejecting both truncated frames and buffers with trailing bytes. Previously a coalesced socket read holding a second frame, or garbage after the LRC, was silently accepted as a single valid APPLICATION packet and the remainder dropped. Splitting a byte stream into frames is the transport layer's responsibility; DecodedPacket has no remainder field. Adds regression tests for trailing-bytes and two coalesced frames. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: ECR17 spec-compliance, build fixes, C++ unit tests + CI
Overview
This PR audits the initial C++ ECR17 protocol stack against the official Nexi
Traditional POS / ECR17 specification (vendored under
docs/), fixes everycorrectness/build defect found, adds a 34-case unit + flow test suite wired into
CI, runs Nitrogen codegen to validate the generated bindings, and ships a
complete package README.
It also supersedes and resolves the Copilot review report in #2 (see the
mapping table below), which is being closed in favour of these concrete fixes.
1. Spec doubts verified (no change needed)
's'(0x73) — confirmed correct(Terminal status request, field 10).
'P'message layout — confirmed byte-exact: 167 bytes with thedocumented field order and
'0'/ space padding.2. Bugs & discrepancies fixed
Ecr17Client.cppstatus()non-void with noreturn→ UB / garbage struct to JSEcr17Protocol.hppEcr17Protocol::buildStatusMessagein a member decl → Clang compile errorPacketCodec.cppsize < 2→ UB/crashUNKNOWNPacketCodec.cppdata.back(); truncated frames mis-validatedETX, require its presencePacketCodec.cpp<algorithm>/<iterator>forstd::find/std::distancePacketCodec.hpp#include "Lcr.hpp"does not resolve from the../cppinclude root#include "Lcr/Lcr.hpp"(matches repo convention)PacketCodec.hppPacketType::STATUS(it is a progress update per spec)PacketType::PROGRESSEcr17Protocol.cppleftPadsilently emitted over-long fixed-width fields; negative amounts injected-Transport/Transport.cppandroid/CMakeLists.txtEcr17.cppcompiled →HybridEcr17Clientsymbols undefined at link on Android (iOS globs via podspec).cppsourcesbiome.jsonpackages/**(plural) vs actualpackage/→ main package never lintedpackage/**3. New feature
Ecr17Protocol::buildReversalMessage(command'S', "annullamento") —26-byte layout per spec, STAN defaults to
"000000"(reverse last, no STANcheck).
4. Nitrogen codegen — validated
Ran
nitrogen 0.35.8(bun run codegenpath). Results:LrcMode.hppisenum class LrcMode { STX=0, STD=1, NOEXT=2, STX_NOEXT=3 }— exactly what
Lcr.cpp(and the test stub) assume. No casing drift.HybridEcr17ClientSpecvirtuals (configure(const Ecr17Config&),configuration(),status()) match theHybridEcr17Clientoverrides.nitrogen/generated/**is gitignored by design (regenerated bybun run codegen), so it is intentionally not part of this PR. Finding fix: ECR17 spec-compliance, build fixes, C++ unit tests + CI #3of the review ("missing generated artifacts") is an inherent codegen step,
now documented in the README.
5. Tests & CI
package/cpp/tests/— buildswithout Nitro via a documented test-only
stubs/LrcMode.hpp..github/workflows/cpp-tests.ymlbuilds & runs it on every PR.Lrc: base value, all four modes, known vectors, string/vector parity,independent reference cross-check.
PacketCodec: app encode framing + round-trip per mode, corrupted-LRCdetection, control (ACK/NAK), and every edge case fixed here (empty, lone
SOH no-crash, progress frame, STX-without-ETX, ETX-without-LRC).
Ecr17Protocol: exact 167-byte Payment layout, 10-byte status layout,26-byte reversal layout, amount boundary, and all validation throws.
ACK), NAK retransmission, reversal/annullamento, pay → reverse → repay,
status round-trip.
6. Docs
README.md: badges (incl. CI), TOC, honest feature-statusmatrix, junior-proof quick start,
Ecr17Configreference, LRC-mode andcommand-code cheat-sheets, architecture, test instructions, roadmap.
docs/.Resolution of the Copilot review (#2)
status()non-void no returnnitrogen/generatedartifactsvalidLrc=truethere is correctstd::invalid_argument/runtime_error); full JS/native error model tracked on the roadmap with the transportTransport.cppsuspect/incompletepackages/**→package/**)Two findings are intentionally documented rather than code-changed (#4, #7):
both depend on the not-yet-built transport layer; ripping out the planned config
surface would be destructive churn, so they're tracked in the README roadmap and
called out here for the reviewer.
Implementation status — what's still missing
XpicHUCTGERK.decode()classifies + extracts rawpayload only).
Transportimpl, no send/receiveorchestration, no ACK/NAK retransmit-up-to-3 loop;
status()is therefore astub.
🤖 Generated with Claude Code