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
20 changes: 20 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ add_library(ariec61850_core
src/mms/data_codec.cpp
src/mms/pdu.cpp
src/mms/services.cpp
src/mms/file_service.cpp
src/mms/dynamic_data_set.cpp
src/mms/dynamic_report_planner.cpp
src/mms/invoke_router.cpp
Expand Down Expand Up @@ -133,6 +134,12 @@ if(ARIEC61850_BUILD_TOOLS)
target_compile_features(ariec61850_pagination_probe PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_pagination_probe)
ariec61850_apply_sanitizers(ariec61850_pagination_probe)

add_executable(ariec61850_file_transfer tools/file_transfer.cpp)
target_link_libraries(ariec61850_file_transfer PRIVATE ARIEC61850::core)
target_compile_features(ariec61850_file_transfer PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_file_transfer)
ariec61850_apply_sanitizers(ariec61850_file_transfer)
endif()

if(ARIEC61850_BUILD_TESTS)
Expand Down Expand Up @@ -210,6 +217,15 @@ if(ARIEC61850_BUILD_TESTS)
ariec61850_apply_sanitizers(ariec61850_mms_services_tests)
add_test(NAME ariec61850_mms_services_tests COMMAND ariec61850_mms_services_tests)

add_executable(ariec61850_mms_file_service_tests tests/test_mms_file_service.cpp)
target_link_libraries(ariec61850_mms_file_service_tests PRIVATE ARIEC61850::core)
target_compile_definitions(ariec61850_mms_file_service_tests PRIVATE
ARIEC61850_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
target_compile_features(ariec61850_mms_file_service_tests PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_mms_file_service_tests)
ariec61850_apply_sanitizers(ariec61850_mms_file_service_tests)
add_test(NAME ariec61850_mms_file_service_tests COMMAND ariec61850_mms_file_service_tests)

add_executable(ariec61850_dynamic_data_set_tests tests/test_dynamic_data_set.cpp)
target_link_libraries(ariec61850_dynamic_data_set_tests PRIVATE ARIEC61850::core)
target_compile_features(ariec61850_dynamic_data_set_tests PRIVATE cxx_std_20)
Expand Down Expand Up @@ -291,6 +307,9 @@ if(ARIEC61850_BUILD_TESTS)
add_test(
NAME ariec61850_pagination_probe_help
COMMAND ariec61850_pagination_probe --help)
add_test(
NAME ariec61850_file_transfer_help
COMMAND ariec61850_file_transfer --help)
endif()
endif()

Expand Down Expand Up @@ -320,5 +339,6 @@ if(ARIEC61850_BUILD_FUZZERS)
ariec61850_add_fuzzer(ariec61850_fuzz_transport fuzz/fuzz_transport.cpp)
ariec61850_add_fuzzer(ariec61850_fuzz_association fuzz/fuzz_association.cpp)
ariec61850_add_fuzzer(ariec61850_fuzz_mms_services fuzz/fuzz_mms_services.cpp)
ariec61850_add_fuzzer(ariec61850_fuzz_mms_file_service fuzz/fuzz_mms_file_service.cpp)
ariec61850_add_fuzzer(ariec61850_fuzz_reporting fuzz/fuzz_reporting.cpp)
endif()
20 changes: 16 additions & 4 deletions MIGRATION_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,17 @@ behavioral oracle until laboratory interoperability is complete.

- [ ] Direct and SBO control state machines.
- [ ] Enhanced-security termination/error handling.
- [ ] MMS file and fault-record transfer.
- [x] MMS FileDirectory `[77]` codec with root/nested paths, attributes, `moreFollows`, and `continueAfter`.
- [x] Bounded FileDirectory pagination with deterministic order-preserving deduplication and no-progress detection.
- [x] MMS FileOpen/FileRead/FileClose `[72-74]` codecs with signed Integer32 FRSM parity.
- [x] Caller-owned streaming sink runtime with byte/read/block/diagnostic bounds and advertised-size validation.
- [x] Best-effort FileClose after read, validation, sink, and observed cancellation failures without replacing the primary error.
- [x] Precise file/file-non-existent rooted-backslash fallback before the first FileRead only.
- [x] Host list/download CLI, JSON evidence, dedicated regression suite, mutation smoke, and libFuzzer corpus.
- [x] Controlled live FileDirectory and small-file download with successful FileClose and local hash evidence.
- [x] Controlled live forced block-limit failure with zero local bytes and successful FileClose cleanup.
- [ ] Multi-page physical FileDirectory continuation evidence.
- [ ] Multi-vendor file-service acceptance and ESP32 device validation.

## Phase 5 — simulator and applications

Expand All @@ -160,6 +170,8 @@ behavioral oracle until laboratory interoperability is complete.

## Safety gate

SCL, COMTRADE, PCAP, and the Phase 4C/4C.1 live discovery surface are read-only. Active RCB
mutation, control, dynamic DataSet mutation, file transfer, Npcap transmission, and real-time SV
scheduling require separate explicit APIs, authorization, and physical-laboratory acceptance.
SCL, COMTRADE, PCAP, and the Phase 4C/4C.1 live discovery surface are read-only. MMS file
download is also remote-content read-only but has an active FileOpen/FileRead/FileClose resource
lifecycle, so the host CLI requires an explicit remote path/destination and controlled-lab
authorization. RCB mutation, control, dynamic DataSet mutation, remote file mutation, Npcap
transmission, and real-time SV scheduling retain separate explicit safety gates.
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ MCU-safe protocol boundary.
- SCL read-only parsing and COMTRADE CFG/DAT support for host engineering tools.
- RFC 1006 TPKT, COTP, ISO Session, Presentation, ACSE, and MMS Initiate.
- Confirmed MMS GetNameList, GetVariableAccessAttributes, Read, and Write codecs.
- Read-only MMS FileDirectory/FileOpen/FileRead/FileClose codecs and bounded streaming runtime.
- DataSet directory, InformationReport, RCB state, and report-subscription foundations.

### Phase 1A–1E — deterministic process-bus core
Expand Down Expand Up @@ -126,6 +127,22 @@ GetVariableAccessAttributes, GetNamedVariableListAttributes and Read. It does
**not** authorize or perform Write, control, GI, RCB reservation/enable,
dynamic DataSet mutation, or file-service mutation.

### Phase 4E - MMS file transfer client

`ariec61850_file_transfer` adds an explicit read-only file-service surface:

- root/nested FileDirectory with bounded continuation;
- streaming FileOpen/FileRead/FileClose download to a caller-owned sink;
- full signed Integer32 FRSM support;
- exact Confirmed-Error evidence and bounded diagnostics;
- best-effort FileClose that preserves the primary failure; and
- a single oracle-matched rooted-backslash fallback for file-non-existent.

The default CLI operation is directory listing. Download requires both
`--remote` and `--output`; upload/delete/rename are not implemented. See
`docs/MMS_FILE_TRANSFER.md` for API, safety, CLI, exit-code, and acceptance
details.

#### Public Alpha A1 model work

The live model now projects C#-style:
Expand Down Expand Up @@ -312,7 +329,9 @@ roles are documented in `docs/EMBEDDED_TARGETS.md`:
- STM32H7/NXP class: later portability/industrialization target.

The embedded profile already excludes host TCP/live-discovery/PCAP/COMTRADE/SCL
parser tooling and provides a no-RTTI host-simulation gate. Sampled Values also
parser tooling and provides a no-RTTI host-simulation gate. The portable MMS
file-service runtime is included through abstract transport/sink boundaries;
its filesystem-backed CLI remains host-only. Sampled Values also
has a caller-owned `encode_into(span)` path so the eventual publisher does not
need to allocate a fresh Ethernet frame buffer per sample.

Expand Down
5 changes: 3 additions & 2 deletions docs/FEATURE_MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ Status terms are intentionally conservative:
| Embedded | ESP32-S3 8DI/8DO IED application | Planned | Waveshare board application reference after bounded MMS server foundation exists. |
| MMS server | Association acceptor + static browse/read model | Planned | Required for the ESP32 I/O IED; not yet a public server feature. |
| MMS mutation | Write/control | Codec/offline foundation only | Live Phase 4C discovery intentionally does not send these operations. |
| File services | File directory/read/write | Planned for later parity | No file-service mutation/request is part of current live acceptance. |
| File services | FileDirectory / FileOpen / FileRead / FileClose client | Live-proven / read-only | Bounded portable streaming runtime, signed FRSM, exact error evidence, rooted-backslash fallback, success cleanup, and forced-failure cleanup were exercised on the controlled lab target. Current directory fit one page; multi-page live continuation remains pending. |
| File services | Upload / delete / rename | Not implemented | Remote file mutation is deliberately outside the current milestone. |
| Security | ASan/UBSan | CI-gated | Security workflow; latest tranche must be green before acceptance claim. |
| Security | libFuzzer corpora | CI-gated | BER, GOOSE, SV, PCAP, SCL, COMTRADE, OSI, MMS services/reporting. |
| Security | libFuzzer corpora | CI-gated | BER, GOOSE, SV, PCAP, SCL, COMTRADE, OSI, MMS services/reporting/file-service. |
| Portability | GCC / Clang / MSVC | CI-gated | Required for host Public Alpha. |
| Acceptance | Same-IED repeated structural stability | In progress | Acceptance runner uses `structuralFingerprint`, not mutable legacy canonical fingerprint. Multi-cycle live evidence is the next A1 acceptance tranche. |
| Acceptance | Timeout/reconnect evidence | In progress | Dedicated controlled failure/reconnect runs remain required; successful association cycles alone are not sufficient evidence. |
Expand Down
135 changes: 135 additions & 0 deletions docs/MMS_FILE_TRANSFER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# MMS file transfer client

## Status and scope

The C++20 core implements the read/download side of the ISO 9506 MMS file
service used by IEC 61850 engineering clients:

- `FileDirectory` confirmed service `[77]`;
- `FileOpen` confirmed service `[72]`;
- `FileRead` confirmed service `[73]`;
- `FileClose` confirmed service `[74]`;
- bounded pagination with `continueAfter`;
- the full signed `Integer32` FRSM identifier range;
- one precise rooted-backslash interoperability fallback; and
- caller-owned streaming sinks with best-effort remote cleanup.

Upload, FileDelete, FileRename, and every other remote file mutation are out of
scope. The implementation is not a claim of full IEC 61850 conformance or
multi-vendor interoperability.

## Architecture

`include/ariec61850/mms/file_service.hpp` and
`src/mms/file_service.cpp` contain the portable layer. They reuse the existing
BER, MMS PDU, Presentation, invoke-routing, and association runtime. They do not
open sockets, create threads, or access a filesystem.

The portable boundaries are:

- `MmsFileServiceCodec` for request/response wire models;
- `MmsFileServiceChannel` for a confirmed-exchange provider;
- `MmsAssociationFileServiceChannel` for the existing association runtime;
- `MmsFileSink` for application-owned flash, SD, RAM, or other storage; and
- `MmsFileTransferRuntime` for directory and download lifecycle policy.

The host-only `ariec61850_file_transfer` executable supplies the native TCP
transport and a filesystem-backed sink. It is not part of `embedded_core`.

## Wire and interoperability behavior

Normal `FileOpen` encodes a normalized path as a sequence of GraphicString path
segments. Backslashes received from the caller are normalized to `/`; empty,
traversal (`.` or `..`), control-character, null-character, drive/scheme-like,
and overlong paths are rejected before an exchange.

Some deployed relays list a root file normally but require FileOpen to use one
GraphicString containing a leading backslash. Adaptive download first sends the
normal segmented request. It retries exactly once with a rooted path only when:

1. FileOpen returned Confirmed-Error `errorClass [11]`, value `7`
(`file/file-non-existent`);
2. no FileRead operation ran and no byte was accepted; and
3. the caller-provided sink resets successfully.

Access denied, malformed responses, transport faults, cancellation, any error
after FileRead, or any other service-error value never triggers the fallback.
Primary and fallback diagnostics are retained separately.

FRSM identifiers are signed 32-bit values. Negative values and both Integer32
endpoints are accepted from FileOpen and echoed unchanged by FileRead/FileClose.
A FileOpen size attribute of zero is treated as unavailable, matching the C#
oracle and deployed relay behavior; a positive advertised size remains an
enforced upper bound.

## Bounded lifecycle and failure rules

Callers configure explicit bounds for directory pages, entries, read
operations, total bytes, block bytes, diagnostic entries, and diagnostic text.
The runtime also rejects:

- repeated/no-progress directory pages;
- an empty FileRead block with `moreFollows=true`;
- arithmetic or configured byte-limit overflow;
- bytes beyond a positive advertised file size; and
- declared-size mismatch when strict matching is requested.

After FileOpen succeeds, FileClose is attempted even after read validation,
sink failure, cancellation observed between operations, or a normal pipeline
failure. Cleanup uses a fresh stop token so caller cancellation does not skip it.
If the underlying association has already faulted or timed out, the result
records that cleanup could not be sent. A FileClose failure never replaces an
earlier primary failure. `remote_file_closed` makes the cleanup outcome explicit.

## Host CLI

Build with `ARIEC61850_BUILD_TOOLS=ON`, then list the root directory:

```text
ariec61850_file_transfer <host> [port] list --json-output evidence.json
```

Download requires both an explicit remote path and destination:

```text
ariec61850_file_transfer <host> [port] download \
--remote REMOTE_PATH --output LOCAL_FILE \
--max-bytes 1048576 --json-output evidence.json
```

Exit codes are stable:

- `0`: requested operation completed;
- `2`: argument/configuration error;
- `3`: association, transport, protocol, decode, or limit failure;
- `4`: local sink failure; and
- `5`: cleanup was the terminal failure.

The default command is FileDirectory. The CLI never uploads, deletes, or
renames a remote object. JSON evidence may contain endpoint/file metadata and
should remain in an ignored local evidence directory unless deliberately
sanitized.

## Validation evidence

Offline regression coverage includes golden request/response shapes, nested and
root directory paths, pagination/continuation, attributes, malformed/truncated/
trailing BER, invoke mismatch, all signed FRSM boundaries, multi-block reads,
empty-progress blocks, configured and advertised size bounds, sink failures,
cancellation/timeout evidence, cleanup precedence, and adaptive fallback policy.
A dedicated mutation target and corpus exercise all four response decoders.

The current controlled lab acceptance proved:

- one bounded root FileDirectory operation;
- a small read-only file downloaded in one FileRead;
- the deployed rooted-backslash FileOpen fallback;
- zero-size FileOpen attribute interoperability;
- successful FileClose after both success and forced block-limit failure; and
- exact local byte count plus SHA-256 evidence.

Exact endpoint, remote filename, file content/hash, and JSON traces are retained
locally in the ignored evidence directory and are intentionally not published.
Pagination was not observed on the current one-page directory and remains a
multi-page live acceptance item. ESP32 compilation/device execution and
multi-vendor physical acceptance also remain pending.
5 changes: 4 additions & 1 deletion embedded/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ set(ARIEC61850_EMBEDDED_SOURCES
${ARIEC61850_ROOT}/src/mms/data_value.cpp
${ARIEC61850_ROOT}/src/mms/data_codec.cpp
${ARIEC61850_ROOT}/src/mms/pdu.cpp
${ARIEC61850_ROOT}/src/mms/services.cpp)
${ARIEC61850_ROOT}/src/mms/services.cpp
${ARIEC61850_ROOT}/src/mms/invoke_router.cpp
${ARIEC61850_ROOT}/src/mms/association_runtime.cpp
${ARIEC61850_ROOT}/src/mms/file_service.cpp)

add_library(ariec61850_embedded_core STATIC ${ARIEC61850_EMBEDDED_SOURCES})
add_library(ARIEC61850::embedded_core ALIAS ariec61850_embedded_core)
Expand Down
6 changes: 6 additions & 0 deletions embedded/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ Host-only facilities remain outside this build profile:
- filesystem access;
- desktop diagnostics/UI.

The portable MMS file-service codec and bounded transfer runtime are included in
`embedded_core`. They use the abstract, caller-supplied `MmsByteTransport` and
`MmsFileSink`; the native TCP transport and filesystem-backed host CLI remain
excluded. This is a host-simulation portability result, not yet an ESP32 device
validation or deterministic no-heap claim.

## Current compiler contract

The embedded profile currently enforces:
Expand Down
1 change: 1 addition & 0 deletions fuzz/corpus/mms_file_service/file_close_response.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A1060201019F4A00
1 change: 1 addition & 0 deletions fuzz/corpus/mms_file_service/file_directory_response.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A105020101BF4D00810100
1 change: 1 addition & 0 deletions fuzz/corpus/mms_file_service/file_open_response.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A108020101BF4803800101
1 change: 1 addition & 0 deletions fuzz/corpus/mms_file_service/file_read_response.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A10B020101BF4906800100810100
15 changes: 15 additions & 0 deletions fuzz/fuzz_mms_file_service.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-3.0-or-later

#include "fuzz_targets.hpp"

#include <cstddef>
#include <cstdint>
#include <span>

extern "C" int LLVMFuzzerTestOneInput(
const std::uint8_t* data,
const std::size_t size) {
ar::iec61850::fuzzing::exercise_mms_file_service(
std::span<const std::uint8_t>{data, size});
return 0;
}
32 changes: 32 additions & 0 deletions fuzz/fuzz_targets.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "ariec61850/evidence/pcap_equivalence.hpp"
#include "ariec61850/goose/frame_codec.hpp"
#include "ariec61850/goose/pdu_codec.hpp"
#include "ariec61850/mms/file_service.hpp"
#include "ariec61850/mms/invoke_router.hpp"
#include "ariec61850/mms/pdu.hpp"
#include "ariec61850/mms/reporting.hpp"
Expand Down Expand Up @@ -348,6 +349,37 @@ inline void exercise_mms_services(const std::span<const std::uint8_t> bytes) {
}
}

inline void exercise_mms_file_service(const std::span<const std::uint8_t> bytes) {
std::uint32_t invoke_id = 1U;
try {
const auto envelope = mms::MmsPduCodec::decode_envelope(bytes);
if (envelope.invoke_id && *envelope.invoke_id != 0U) {
invoke_id = *envelope.invoke_id;
}
} catch (...) {
}
try {
static_cast<void>(mms::MmsFileServiceCodec::decode_file_directory_response(
bytes, invoke_id));
} catch (...) {
}
try {
static_cast<void>(mms::MmsFileServiceCodec::decode_file_open_response(
bytes, invoke_id));
} catch (...) {
}
try {
static_cast<void>(mms::MmsFileServiceCodec::decode_file_read_response(
bytes, invoke_id));
} catch (...) {
}
try {
static_cast<void>(mms::MmsFileServiceCodec::decode_file_close_response(
bytes, invoke_id));
} catch (...) {
}
}


inline void exercise_reporting(const std::span<const std::uint8_t> bytes) {
try {
Expand Down
Loading
Loading