Skip to content

fix: ECR17 spec-compliance, build fixes, C++ unit tests + CI#3

Merged
47PADO47 merged 6 commits into
mainfrom
fix/ecr17-spec-compliance
May 28, 2026
Merged

fix: ECR17 spec-compliance, build fixes, C++ unit tests + CI#3
47PADO47 merged 6 commits into
mainfrom
fix/ecr17-spec-compliance

Conversation

@lopadova
Copy link
Copy Markdown
Contributor

@lopadova lopadova commented May 27, 2026

Overview

This PR audits the initial C++ ECR17 protocol stack against the official Nexi
Traditional POS / ECR17
specification (vendored under docs/), fixes every
correctness/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)

  • Status message code is lowercase 's' (0x73) — confirmed correct
    (Terminal status request, field 10).
  • Payment 'P' message layout — confirmed byte-exact: 167 bytes with the
    documented field order and '0' / space padding.

2. Bugs & discrepancies fixed

# File Problem Fix
1 Ecr17Client.cpp status() non-void with no return → UB / garbage struct to JS Throws a clear "not implemented" error until transport+parsing exist
2 Ecr17Protocol.hpp Illegal qualified name Ecr17Protocol::buildStatusMessage in a member decl → Clang compile error Removed the qualifier
3 PacketCodec.cpp SOH branch built a string from an inverted iterator range when size < 2 → UB/crash Bounds guard → UNKNOWN
4 PacketCodec.cpp LRC read as data.back(); truncated frames mis-validated Read the byte right after ETX, require its presence
5 PacketCodec.cpp Missing <algorithm>/<iterator> for std::find/std::distance Added includes
6 PacketCodec.hpp #include "Lcr.hpp" does not resolve from the ../cpp include root #include "Lcr/Lcr.hpp" (matches repo convention)
7 PacketCodec.hpp SOH frame mislabeled PacketType::STATUS (it is a progress update per spec) Renamed to PacketType::PROGRESS
8 Ecr17Protocol.cpp leftPad silently emitted over-long fixed-width fields; negative amounts injected - Throw on field overflow / negative amount
9 Transport/Transport.cpp Byte-for-byte duplicate of the header for a pure-abstract interface Removed the file
10 android/CMakeLists.txt Only Ecr17.cpp compiled → HybridEcr17Client symbols undefined at link on Android (iOS globs via podspec) Added the new .cpp sources
11 biome.json Lint glob packages/** (plural) vs actual package/ → main package never linted Fixed to package/**

3. New feature

  • Ecr17Protocol::buildReversalMessage (command 'S', "annullamento") —
    26-byte layout per spec, STAN defaults to "000000" (reverse last, no STAN
    check).

4. Nitrogen codegen — validated

Ran nitrogen 0.35.8 (bun run codegen path). Results:

  • ✅ Generated LrcMode.hpp is enum class LrcMode { STX=0, STD=1, NOEXT=2, STX_NOEXT=3 }
    exactly what Lcr.cpp (and the test stub) assume. No casing drift.
  • HybridEcr17ClientSpec virtuals (configure(const Ecr17Config&),
    configuration(), status()) match the HybridEcr17Client overrides.
  • ℹ️ nitrogen/generated/** is gitignored by design (regenerated by
    bun run codegen), so it is intentionally not part of this PR. Finding fix: ECR17 spec-compliance, build fixes, C++ unit tests + CI #3
    of the review ("missing generated artifacts") is an inherent codegen step,
    now documented in the README.

5. Tests & CI

  • New standalone GoogleTest suite under package/cpp/tests/ — builds
    without Nitro via a documented test-only stubs/LrcMode.hpp.
  • .github/workflows/cpp-tests.yml builds & runs it on every PR.
  • 34/34 green in CI. Coverage:
    • Lrc: base value, all four modes, known vectors, string/vector parity,
      independent reference cross-check.
    • PacketCodec: app encode framing + round-trip per mode, corrupted-LRC
      detection, 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.
    • Documented flows: basic payment (request → ACK → progress → result →
      ACK), NAK retransmission, reversal/annullamento, pay → reverse → repay,
      status round-trip.

6. Docs

  • Full package README.md: badges (incl. CI), TOC, honest feature-status
    matrix, junior-proof quick start, Ecr17Config reference, LRC-mode and
    command-code cheat-sheets, architecture, test instructions, roadmap.
  • Vendored the protocol reference under docs/.

Resolution of the Copilot review (#2)

# (REVIEW.md) Finding Status here
1 status() non-void no return ✅ Fixed (throws)
2 Invalid C++ method declaration ✅ Fixed
3 Missing nitrogen/generated artifacts ✅ Ran codegen; gitignored by design + documented
4 Public TS config richer than native impl 🟡 Documented (status matrix + notes). Fields are the config contract for the roadmap transport; kept, not removed, to avoid breaking churn
5 Insufficient input validation ✅ Fixed (field width + amount validation, throws)
6 Fragile frame parser (SOH/STX/control) ✅ SOH bounds + STX LRC tightened. Note: SOH (progress) frames have no LRC by spec, so validLrc=true there is correct
7 Near-absent error model 🟡 Improved (status + builders throw std::invalid_argument/runtime_error); full JS/native error model tracked on the roadmap with the transport
8 Transport.cpp suspect/incomplete ✅ Removed (pure-abstract interface needs no TU)
9 Biome lint glob mismatch ✅ Fixed (packages/**package/**)
10 No automated tests ✅ Added 34 tests + CI

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

  • Request builders not yet implemented: X p i c H U C T G
    E R K.
  • Response field parsing: none yet (decode() classifies + extracts raw
    payload only).
  • Transport & flow: no concrete Transport impl, no send/receive
    orchestration, no ACK/NAK retransmit-up-to-3 loop; status() is therefore a
    stub.

🤖 Generated with Claude Code

lopadova and others added 5 commits May 27, 2026 23:41
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>
@lopadova lopadova marked this pull request as ready for review May 27, 2026 22:09
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, STATUSPROGRESS), Ecr17Protocol (field-overflow / negative-amount throws), Ecr17Client::status() (throws instead of UB), and the qualified-name compile error in Ecr17Protocol.hpp.
  • New Ecr17Protocol::buildReversalMessage plus standalone GoogleTest suite (package/cpp/tests/) with a stub LrcMode.hpp and a CI workflow.
  • Build/lint corrections (Android CMakeLists.txt source list, Biome glob packages/**package/**), removal of the duplicate Transport.cpp body, and a full README + vendored spec under docs/.

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>
@47PADO47 47PADO47 merged commit 3acdeba into main May 28, 2026
1 check passed
lopadova pushed a commit that referenced this pull request May 28, 2026
fix: ECR17 spec-compliance, build fixes, C++ unit tests + CI
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants