326 Commits

Author SHA1 Message Date
cb0ddb1338 test fixes 2024-11-08 15:26:26 +01:00
39ec3e795c switch to thiserror completely 2024-11-08 14:53:53 +01:00
3b920cbdd8 switch to thiserror completely 2024-11-08 14:17:36 +01:00
c0b4653c01 Merge pull request 'bump CI msrv check' (#114) from bump-msrv-check into main
Reviewed-on: #114
2024-11-08 11:27:52 +01:00
f156833985 bump CI msrv check 2024-11-08 11:26:51 +01:00
9aea3dba00 Merge pull request 'bump dependencies' (#113) from bump-dependencies into main
Reviewed-on: #113
2024-11-08 11:14:04 +01:00
48247a0a87 bump thiserror and zerocopy 2024-11-08 11:13:41 +01:00
f70b957d9a Merge pull request 'docs fixes' (#112) from smaller-doc-fixes into main
Reviewed-on: #112
2024-11-07 23:28:41 +01:00
fbf953df0e docs fixes 2024-11-04 11:42:51 +01:00
f135d54364 Merge pull request 'prepare v0.12.0' (#111) from prepare-v0.12.0 into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #111
2024-09-10 17:58:03 +02:00
d8b2a3dfea prepare v0.12.0
Some checks are pending
Rust/spacepackets/pipeline/head Build started...
2024-09-10 17:51:31 +02:00
448b76be91 Merge pull request 'condition code bugfix' (#110) from cfdp-cond-code-bugfix into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #110
2024-08-29 09:47:27 +02:00
027b01f00f condition code bugfix
Some checks are pending
Rust/spacepackets/pipeline/head Build queued...
2024-08-29 09:46:40 +02:00
bf15b22889 Merge pull request 'added max file segment length calculator' (#109) from file-segment-calculator into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #109
2024-08-21 14:29:16 +02:00
16f91b562d added max file segment length calculator
Some checks are pending
Rust/spacepackets/pipeline/head Build started...
2024-08-21 14:26:11 +02:00
cd77b806fe Merge pull request 'Added additional converter method' (#108) from msgs-to-user-converter-method into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #108
2024-08-21 11:20:33 +02:00
43c88da3f2 Added additional converter method
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-08-20 17:24:53 +02:00
b19a61b859 Merge pull request 'update msg to user module' (#107) from cfdp-msg-to-user-update into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #107
2024-08-20 17:17:11 +02:00
8aa957b8bb update msg to user module
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-08-20 16:56:25 +02:00
190fa1befc Merge pull request 'Added generic sequence counter module' (#106) from seq-count-module into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #106
2024-08-20 11:20:07 +02:00
175b61deca Added generic sequence counter module
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-08-20 10:57:53 +02:00
51c28b5cc6 Merge pull request 'Github MSRV version update' (#105) from github-msrv into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #105
2024-08-19 10:58:31 +02:00
45cc74daa7 Github MSRV version update
Some checks are pending
Rust/spacepackets/pipeline/head Build started...
2024-08-19 10:44:33 +02:00
191c6f8146 Merge pull request 'Bump MSRV and delegate version' (#104) from bump-msrv-delegate-version into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #104
2024-08-19 10:42:29 +02:00
5449884b2e Bump MSRV and delegate version
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-08-19 02:23:34 -06:00
9c93c76193 Merge pull request 'Update EOF PDU API' (#103) from eof-pdu-update into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #103
2024-08-19 10:18:19 +02:00
043927c7ef Update EOF PDU API
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-07-21 10:14:41 -07:00
f4dc5a0302 Merge pull request 'added new API for file data PDU' (#102) from file-data-pdu-update into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #102
2024-07-21 18:25:08 +02:00
9166faa4ae optimization
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-07-19 11:29:37 -07:00
ed808e69d4 added new API for file data PDU
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-07-19 10:41:31 -07:00
d146b6cf57 Merge pull request 'Metadata PDU creator update' (#101) from metadata-pdu-creator-update into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #101
2024-07-14 17:08:46 +02:00
ff0c9d8c70 Update and simplify Metadata PDU creator API
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-07-09 16:30:48 +02:00
c40bc855a2 Merge pull request 'add owned TLV type' (#98) from cfdp-tlv-owned-type into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #98
2024-07-09 16:08:53 +02:00
81423fc6e8 add owned TLV type
Some checks are pending
Rust/spacepackets/pipeline/head Build queued...
Rust/spacepackets/pipeline/pr-main Build queued...
2024-07-09 16:04:08 +02:00
a399b11a8e Merge pull request 'update documentation build' (#99) from update-docs-build into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #99
2024-07-03 16:14:46 +02:00
9d4c7446a3 Merge branch 'main' into update-docs-build
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-06-25 16:20:08 +02:00
b87f7d73b1 Merge pull request 'clippy fix' (#100) from clippy-fix into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #100
2024-06-25 16:20:01 +02:00
80744eea16 clippy fix
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2024-06-25 16:19:30 +02:00
a5918bfd4a update documentation build
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-06-25 16:07:07 +02:00
0e347b0e37 Merge pull request 'Bump MSRV' (#97) from bump-msrv into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #97
2024-05-19 13:07:12 +02:00
58dabb6f2f specify exact required version
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-05-19 09:13:12 +02:00
7fd65aa592 bumped MSRV
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-05-19 09:12:39 +02:00
0024afc83e Merge pull request 'prep patch release' (#96) from prep-v0.11.2 into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #96
2024-05-19 09:02:46 +02:00
c48bd848d3 prep patch release
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-05-19 08:49:03 +02:00
b8be9ae641 Merge pull request 'Fixes for Miri' (#95) from fixes-for-miri into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #95
2024-05-15 13:03:24 +02:00
c2506dbba9 Merge branch 'main' into fixes-for-miri
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-05-14 19:25:07 +02:00
b842b9d11a Merge pull request 'remove defmt::Format impl for MetadataPduCreator' (#94) from fix-defmt-derives into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #94
2024-05-14 19:24:57 +02:00
374c034e92 add miri chapter in README
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-05-14 15:37:20 +02:00
791c7f6e02 it is now possible to run cargo miri 2024-05-14 15:34:40 +02:00
8001938507 remove defmt::Format impl for MetadataPduCreator
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-05-14 15:01:26 +02:00
73ab7ff148 Merge pull request 'add doctests to github CI' (#93) from github-ci-doctest into main
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Reviewed-on: #93
2024-05-02 14:56:13 +02:00
c59d01174f add doctests to github CI
Some checks are pending
Rust/spacepackets/pipeline/head Build queued...
2024-05-02 14:48:31 +02:00
eb49bff0c9 Merge pull request 'update github CI' (#92) from update-github-ci into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #92
2024-05-02 14:29:53 +02:00
af392d40d0 this might work
Some checks are pending
Rust/spacepackets/pipeline/head Build queued...
Rust/spacepackets/pipeline/pr-main Build queued...
2024-05-02 14:22:03 +02:00
b78bfe2114 some fixes
Some checks are pending
Rust/spacepackets/pipeline/head Build queued...
Rust/spacepackets/pipeline/pr-main Build queued...
2024-05-02 14:16:20 +02:00
69a3b1d8f3 update github CI
Some checks are pending
Rust/spacepackets/pipeline/pr-main Build queued...
Rust/spacepackets/pipeline/head Build started...
2024-05-02 14:12:26 +02:00
e7b3ba9575 Merge pull request 'date correction' (#91) from date-correction into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #91
2024-04-22 10:19:19 +02:00
c515535ccd date correction
Some checks are pending
Rust/spacepackets/pipeline/head Build queued...
2024-04-22 10:18:35 +02:00
95158a8cd2 Merge pull request 'prepare next patch version' (#90) from small-improvements-and-fixes into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #90
2024-04-22 10:15:21 +02:00
8b1ccb0cd0 prepare next patch version 2024-04-20 10:42:36 +02:00
619b22e58f Merge pull request 'prepare v0.11.0' (#89) from prep_v0.11.0 into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #89
2024-04-16 19:23:17 +02:00
55222d92b3 small typo fix
Some checks are pending
Rust/spacepackets/pipeline/head Build started...
2024-04-16 19:17:17 +02:00
8e1934e604 prepare release v0.11.0
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-04-16 19:15:04 +02:00
5f37978c56 Merge pull request 'added small defmt test' (#88) from added-small-defmt-test into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #88
2024-04-16 15:34:41 +02:00
97bbb14168 Merge branch 'main' into added-small-defmt-test 2024-04-16 15:34:34 +02:00
a65a98f43f Merge pull request 'clippy and msrv fix' (#87) from ci-github-fixes into main
Reviewed-on: #87
2024-04-16 15:34:27 +02:00
e1a200e65b Merge branch 'main' into added-small-defmt-test
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-04-16 15:14:23 +02:00
b55c7db3fc clippy and msrv fix
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-04-16 15:13:43 +02:00
944bcf1320 Merge pull request 'bump MSRV' (#86) from bump-msrv into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #86
2024-04-13 18:48:45 +02:00
8972dcbfc0 bump MSRV
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head This commit looks good
2024-04-13 17:39:53 +02:00
04b671fa6f Merge pull request 'moved CCSDS constant' (#85) from move-ccsds-constant into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #85
2024-04-13 17:19:15 +02:00
533afc33fa moved CCSDS constant
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-04-13 12:10:32 +02:00
50c56f6504 added small defmt test
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-04-04 16:35:22 +02:00
9e02e00d1a Merge pull request 'prepare next release candidate' (#84) from prep_v0.11.0-rc.2 into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #84
2024-04-04 14:21:40 +02:00
d8676ae711 prepare next release candidate
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-04-04 14:12:33 +02:00
9711159969 Merge pull request 'use cargo nextest in CI for testing' (#83) from use-nextest-as-test-runner-ci into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #83
2024-04-04 13:20:27 +02:00
57adb619b3 use cargo nextest in CI for testing
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-04-04 13:13:43 +02:00
fe52657d11 Merge pull request 'ECSS Ctors: Expect SP header by copy' (#82) from ecss-take-sp-header-by-copy into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #82
2024-04-04 12:20:45 +02:00
50b86939a1 this API is a bit more ergonomic
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-04-04 12:07:37 +02:00
179984f258 Merge pull request 'More smaller tweaks' (#81) from more-smaller-tweaks into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #81
2024-04-04 11:58:48 +02:00
deb89362a4 More smaller tweaks
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-04-04 11:47:39 +02:00
4cd40f37ce Merge pull request 'added additional ctors which only set the APID' (#80) from addition-sp-header-ctors into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #80
2024-04-03 22:59:19 +02:00
bbd66a6a8b added a lot of inline attrs
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-04-03 21:56:26 +02:00
0115461bb5 added additional ctors which only set the APID
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-04-03 21:30:23 +02:00
ca90393d95 Merge pull request 'unify CCSDS API as well' (#79) from unify-ccsds-api into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #79
2024-04-03 19:45:14 +02:00
325e7d6ff3 unify CCSDS API as well
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-04-03 18:47:00 +02:00
228f198006 Merge pull request 'prepare next release candidate' (#78) from prep_v0.11.0-rc.1 into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #78
2024-04-03 15:07:06 +02:00
54f065ed74 small fix
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-04-03 14:18:12 +02:00
4ef65279ea doc update
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-04-03 14:16:42 +02:00
f0af16dc29 prepare next release candidate
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-04-03 14:13:04 +02:00
d05a1077e8 Merge pull request 'consistent ECSS object constructors' (#77) from consistent-ecss-ctors into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #77
2024-04-03 14:09:36 +02:00
fc684a42a8 consistent ECSS object constructors
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-04-03 13:30:01 +02:00
e9ddc316c8 Merge pull request 'update ECSS code' (#75) from update-ecss-code into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #75
2024-03-29 14:22:43 +01:00
4da417dfd2 cargo fmt
Some checks are pending
Rust/spacepackets/pipeline/pr-main Build started...
Rust/spacepackets/pipeline/head This commit looks good
2024-03-29 14:13:44 +01:00
cabb3a19ef Update ECSS code
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2024-03-29 14:06:52 +01:00
5eef376351 Merge pull request 'Start adding defmt support' (#76) from start-adding-defmt-support into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #76
2024-03-29 14:06:04 +01:00
538548b05e Merge branch 'main' into start-adding-defmt-support
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-03-29 13:50:01 +01:00
caaecdff0c Merge pull request 'Some more API adaptions' (#74) from api-name-updates into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #74
2024-03-29 13:44:03 +01:00
3045a27d8c just add support for everything
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-03-29 13:42:02 +01:00
c7cf83d468 some defmt support would be good
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2024-03-28 22:48:58 +01:00
ef37a84edc Make API more inline with other time API out there
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-03-25 16:08:30 +01:00
c1b32bca21 Merge pull request 'More granular error handling' (#73) from more-granular-error-handling into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #73
2024-03-25 14:18:37 +01:00
d9525674c3 doc fixes
All checks were successful
Rust/spacepackets/pipeline/pr-main This commit looks good
Rust/spacepackets/pipeline/head This commit looks good
2024-03-25 14:05:04 +01:00
8b151d942d Merge remote-tracking branch 'origin/main' into more-granular-error-handling
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-03-25 13:43:19 +01:00
85a8eb3f4a more granular error handling
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head This commit looks good
2024-03-25 13:42:18 +01:00
fb71185b4a Merge pull request 'Introduce automatic doc feature configuration' (#72) from update-docs into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #72
2024-03-25 10:55:34 +01:00
3e62d7d411 introduce doc_auto_cfg
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-03-24 12:22:08 +01:00
3faffd52fc Merge pull request 'Update Time API' (#71) from update-time-api into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #71
2024-03-18 15:57:17 +01:00
7476fc8096 some more fixes and cleaning up
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-03-18 15:23:26 +01:00
59c7ece126 Major refactoring of the time API
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-03-18 15:14:40 +01:00
6f5254bdbd Merge pull request 'More useful conversions' (#68) from missing-ecss-enum-conversion into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #68
2024-03-11 14:57:51 +01:00
bd1927c5c2 use a more generic blanket impl
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-03-11 14:35:09 +01:00
77862868d5 these conversions are also useful
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-03-11 14:28:18 +01:00
ea05a547ac Merge pull request 'add missing doc_cfg attr' (#67) from missing-doc-cfg-attr into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #67
2024-03-04 12:58:28 +01:00
0ab69b3ddc add missing doc_cfg attr
Some checks are pending
Rust/spacepackets/pipeline/head Build started...
2024-03-04 12:55:16 +01:00
240f0bc267 Merge pull request 'CHANGELOG' (#66) from merge-conflict into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #66
2024-03-04 12:52:49 +01:00
00744a22fc Merge branch 'main' of egit.irs.uni-stuttgart.de:rust/spacepackets
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-03-04 12:26:29 +01:00
c5aeeec19f prepare rc.0 2024-03-04 12:25:47 +01:00
d13cd28962 Merge pull request 'add from impls' (#65) from ecss-enum-from-impls into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #65
2024-03-04 12:20:01 +01:00
5641d9007e Merge remote-tracking branch 'origin/main' into ecss-enum-from-impls
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-03-01 17:55:16 +01:00
f39ea2f793 Merge pull request 'improve the time API' (#64) from improve-time-api into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #64
2024-03-01 17:54:31 +01:00
e4730d4b8f changelog
Some checks failed
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2024-03-01 17:54:02 +01:00
64ea7e609d better naming
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-03-01 17:52:51 +01:00
ebaa6210a4 add from impls
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-03-01 17:51:16 +01:00
d14f532f62 improve the time API
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-02-27 15:59:04 +01:00
6ea18d3715 Merge pull request 'added missing doc_cfg attribute' (#63) from add-missing-doc-cfg-attr into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #63
2024-02-19 20:12:15 +01:00
6056342334 added missing doc_cfg attribute
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-02-17 21:01:11 +01:00
4e6dcc5afa Merge pull request 'UnsignedEnum trait extensions' (#62) from unsigned-enum-vec-ext into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #62
2024-02-17 13:41:51 +01:00
200593bfb4 added tests for vec converters
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-02-17 13:27:28 +01:00
60bf876dd3 Extensions for UnsignedEnum trait
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2024-02-17 13:24:25 +01:00
f47604346e Merge pull request 'update PusTmZeroCopyWriter' (#61) from update-pus-tm-zero-copy-writer into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #61
2024-02-07 11:28:55 +01:00
0d0d7a256a update PusTmZeroCopyWriter
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-02-07 11:07:20 +01:00
2fd5860e18 Merge pull request 'v0.8.1' (#60) from prep-v0.8.1 into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #60
2024-02-05 15:32:09 +01:00
7e8b71db6d prep patch release
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-02-05 15:29:18 +01:00
c3cc6d5c73 Merge pull request 'extended time writer trait' (#58) from extend-time-writer-trait into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #58
2024-02-05 15:07:14 +01:00
d01309cccf extended time writer trait
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-02-05 15:04:29 +01:00
92403738ca Merge pull request 'v0.7.0' (#57) from prep_v0.7.0 into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #57
2024-02-01 17:54:35 +01:00
3353475261 prep next release
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-02-01 17:52:45 +01:00
84c1c47fe1 not sure what this does
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-02-01 17:47:25 +01:00
c4bbf91be8 lets keep all features
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-01-31 14:56:41 +01:00
7200e10250 these flags are new
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-01-31 14:55:19 +01:00
66ae83c0ce Merge pull request 'prep next beta release' (#56) from prep_v0.7.0-beta.4 into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #56
2024-01-23 18:39:48 +01:00
2439c9e5fd prep next beta release
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-01-23 17:59:56 +01:00
e992aad52c Merge pull request 'bugfix for metadata PDU creator' (#55) from metadata-pdu-creator-bugfix into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #55
2024-01-23 17:57:52 +01:00
77135af2bc bugfix for metadata PDU creator
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2024-01-23 17:55:07 +01:00
b4e49fdecc Merge pull request 'fix badge link for new website' (#54) from second-badge-fix into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #54
2023-12-13 15:40:58 +01:00
9c1a98139a fix badge link for new website
Some checks are pending
Rust/spacepackets/pipeline/head Build started...
2023-12-13 15:39:41 +01:00
5a773cc0be Merge pull request 'fix deploy path' (#53) from fix-deployment-path into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #53
2023-12-12 11:53:32 +01:00
adab462539 fix deploy path
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-12-12 11:48:00 +01:00
297cfad226 Merge pull request 'Clean up ECSS API' (#52) from clean-up-ecss-api into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #52
2023-12-07 13:53:59 +01:00
efba19db3e Merge remote-tracking branch 'origin/main' into clean-up-ecss-api
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-12-07 13:50:33 +01:00
6017de9ec7 Merge pull request 'This magically fixes the upload' (#51) from cov-deployment into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #51
2023-12-07 13:50:21 +01:00
f57b84b862 clean up ECSS API a bit
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-12-07 13:49:59 +01:00
960835f99d it's the clean?!
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-12-07 13:11:14 +01:00
4b6b935b06 clean
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-12-07 11:09:06 +01:00
c45846819b this is weird
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-07 11:04:58 +01:00
d1b9f4a4d5 that did not help, let's try the clear command
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-12-07 11:03:52 +01:00
5112338263 Merge pull request 'Some more tests' (#50) from some-more-tests into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #50
2023-12-07 11:00:03 +01:00
4ee57c104b delete stray file
Some checks are pending
Rust/spacepackets/pipeline/head Build started...
2023-12-07 01:05:40 +01:00
6d0f71bc12 try with build command 2023-12-07 01:00:07 +01:00
688174e23d Merge pull request 'let's try it with a trailing slash' (#49) from jenkinsfile-fix-possibly into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #49
2023-12-06 20:14:20 +01:00
e4fab72745 let's try it with a trailing slash
Some checks are pending
Rust/spacepackets/pipeline/head Build started...
2023-12-06 20:12:41 +01:00
cfeb74d4c2 Merge pull request 'prepare new beta version' (#48) from prep_v0.7.0-beta.3 into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #48
2023-12-06 20:02:27 +01:00
0ddb9c69f1 jenkinsfile fixes
All checks were successful
Rust/spacepackets/pipeline/pr-main This commit looks good
Rust/spacepackets/pipeline/head This commit looks good
2023-12-06 20:01:36 +01:00
3c72328466 prepare new beta version
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-12-06 18:13:22 +01:00
47a9335495 Merge pull request 'Coverage Update' (#47) from coverage-update into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #47
2023-12-06 18:05:56 +01:00
044ce7a300 changelog update for CUC time
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-12-06 17:53:35 +01:00
8b0a5d1d2c that should do the job
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-12-06 17:50:33 +01:00
9dbb7429e8 let's hope this was the last issue
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-12-06 17:35:28 +01:00
d472c8476a this can not really be deserialized
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-12-06 17:33:28 +01:00
28e9dd9b29 added missing alloc feature gate
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-12-06 17:07:28 +01:00
90cca0fd9e cargo fmt
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-12-06 17:06:30 +01:00
56c3b7474d better finished PDU API
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-06 17:04:30 +01:00
3818dcd46f this is less confusing
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-06 16:17:54 +01:00
38f5e3ba5f something is wrong with the function coverage..
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-06 16:16:21 +01:00
3650507715 some more serde tests
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-06 15:17:03 +01:00
bf13a432b8 add some more serde test
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-06 01:33:26 +01:00
c21ddf3cf0 that reduced coverage again.. 2023-12-06 01:12:33 +01:00
5b7c500ee7 tests green again 2023-12-05 20:50:15 +01:00
059f5ba5f5 simplified CDS impl 2023-12-05 20:47:13 +01:00
c19e8e6464 what is this
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-05 18:35:08 +01:00
ed4c8af164 added FS response deserialization test
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-05 17:06:12 +01:00
9e40dcde95 more stuff to test yay
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-05 16:58:57 +01:00
dc2b97b848 add filestore response abstraction
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-05 16:29:30 +01:00
4945ea804d small test for TC reader invalid input
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-05 15:31:00 +01:00
c6c80edb84 added some more basic tests
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-05 15:26:34 +01:00
f620304b3a fix that test
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-05 15:08:03 +01:00
71e043e159 more time module tests
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-05 15:05:00 +01:00
149b4d65a2 add seq count
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-05 14:01:21 +01:00
fc18a01b4c more tests
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-05 13:50:10 +01:00
08b1ddc41d oh shit gotta go
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-05 10:08:19 +01:00
13b9ca356c added another finished PDU test
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-04 17:55:56 +01:00
299d37d894 introduce new TLV abstractions
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-04 15:54:35 +01:00
7965e71c49 continue coverage imrpvoements 2023-12-04 13:44:53 +01:00
52063320be more
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-12-03 20:25:32 +01:00
a2535502ea move coverage improvements
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-12-03 19:55:09 +01:00
44383c10a8 added some more tests 2023-12-03 19:45:31 +01:00
175315e44e improved coverage a bit 2023-12-03 16:00:28 +01:00
4faf1c99d8 chhangelog
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-12-03 15:46:07 +01:00
7b66061625 remove duplicate error variant 2023-12-03 15:45:11 +01:00
da201a91e5 well this is annoying work
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-12-03 13:05:55 +01:00
834d56c9bd Merge pull request 'add segment metadata accessors' (#46) from seg-metadata-accessors into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #46
2023-12-03 12:38:55 +01:00
f11a23c7c7 Merge branch 'main' into seg-metadata-accessors
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-12-02 14:58:54 +01:00
5205cc0758 Merge pull request 'maybe those links are secure' (#45) from smaller-improvements into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #45
2023-12-02 14:58:33 +01:00
d5f945305d add segment metadata accessors
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-12-02 14:58:03 +01:00
58ec20c629 Merge branch 'smaller-improvements' of egit.irs.uni-stuttgart.de:rust/spacepackets into smaller-improvements
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-12-02 14:45:19 +01:00
2fdf057305 even better 2023-12-02 14:45:12 +01:00
b357fba212 Merge branch 'main' into smaller-improvements
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head This commit looks good
2023-12-02 14:43:39 +01:00
e4acb9fe4f maybe those links are secure 2023-12-02 14:42:54 +01:00
7e85ea7cd1 Merge pull request 'Small improvements' (#44) from smaller-improvements into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #44
2023-12-01 18:33:45 +01:00
60a00bae99 README update
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-12-01 17:21:36 +01:00
7a8c3784f5 improve coverage python script 2023-12-01 17:19:58 +01:00
347d40bcf0 Merge pull request 'split up metadata PDU API' (#42) from metadata-pdu-split-api into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #42
2023-12-01 16:54:23 +01:00
94ed37d596 this should work
All checks were successful
Rust/spacepackets/pipeline/pr-main This commit looks good
Rust/spacepackets/pipeline/head This commit looks good
2023-12-01 16:53:21 +01:00
ec886ba83d README update
All checks were successful
Rust/spacepackets/pipeline/pr-main This commit looks good
Rust/spacepackets/pipeline/head This commit looks good
2023-12-01 16:48:43 +01:00
983d69140a let's try this..
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-12-01 16:42:26 +01:00
be86e3055e I think this stays the same
Some checks failed
Rust/spacepackets/pipeline/pr-main This commit looks good
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-01 16:37:58 +01:00
c7e98a964a that's odd..
Some checks failed
Rust/spacepackets/pipeline/pr-main This commit looks good
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-01 16:35:46 +01:00
490b05e612 that should do the job
Some checks failed
Rust/spacepackets/pipeline/pr-main This commit looks good
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-01 16:28:22 +01:00
c4847850d9 this might work better
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-12-01 16:20:38 +01:00
2ea996b9d0 serde does not work here either..
Some checks failed
Rust/spacepackets/pipeline/pr-main This commit looks good
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-01 16:12:50 +01:00
681271a53c lets try to make serde work
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-12-01 16:02:37 +01:00
4d21a79a46 Merge branch 'metadata-pdu-split-api' of egit.irs.uni-stuttgart.de:rust/spacepackets into metadata-pdu-split-api
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-01 16:00:36 +01:00
0f3cf48c0e update CI 2023-12-01 16:00:29 +01:00
8a78f27d41 Merge branch 'main' into metadata-pdu-split-api
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-12-01 15:35:38 +01:00
48b0362dc1 split up metadata PDU API
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-01 15:35:09 +01:00
80b80f6777 Merge pull request 'Add ACK and NAK PDU abstractions' (#41) from add-ack-nak-pdus into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #41
2023-12-01 15:33:35 +01:00
e355de3f10 fmt
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-12-01 14:46:01 +01:00
1f1aa68485 remove std usages
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-12-01 14:34:16 +01:00
e2ae959d03 serde impl is tricky here..
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-12-01 14:27:21 +01:00
e422f4f969 fix for serde
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-01 14:22:35 +01:00
56113ffbcb NAK PDU crc check
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-01 14:18:33 +01:00
ea1edfb3c1 improve coverage
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-12-01 12:11:31 +01:00
94ff4fbb51 added coverage section in README
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-12-01 10:33:12 +01:00
c99b6fddec added coverage helper script
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-12-01 10:22:44 +01:00
2ba2998426 add more tests
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-11-30 22:59:35 +01:00
80aa963226 that was complicated but it works
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-11-30 20:20:30 +01:00
5ae86619b4 last push
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-11-28 22:33:34 +01:00
b8dacc12b5 add test
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-11-28 22:28:49 +01:00
6ed023d50d split up NAK PDU API
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-11-28 01:19:03 +01:00
65d3b4e4e5 added NAK PDU impl
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-11-27 17:48:05 +01:00
dcb697bf6f added some tests for new ACK PDU
All checks were successful
Rust/spacepackets/pipeline/pr-main This commit looks good
Rust/spacepackets/pipeline/head This commit looks good
2023-11-27 16:25:55 +01:00
c0bedac058 Serialize/Deserialize
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-11-27 15:52:47 +01:00
c0805db137 first test 2023-11-26 23:52:34 +01:00
fdf6e1de90 added ACK PDU impl
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-11-26 21:24:24 +01:00
9976b53f65 added basic state tests for FD and Finished PDU
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-11-26 15:04:44 +01:00
478f8aa216 Merge pull request 'custom impl for CommonPduConfig PartialEq' (#40) from common-pdu-abstractions into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #40
2023-11-25 19:21:22 +01:00
a23e5107f2 fmt
All checks were successful
Rust/spacepackets/pipeline/pr-main This commit looks good
Rust/spacepackets/pipeline/head This commit looks good
2023-11-25 18:16:21 +01:00
7650429c5b custom impl for CommonPduConfig PartialEq
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-11-24 17:09:23 +01:00
5e892f86b3 Merge pull request 'Improvements for writable abstractions' (#39) from improvements-for-writable-abstractions into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #39
2023-11-24 16:36:35 +01:00
b82a93757c add unittests for vec conversion
All checks were successful
Rust/spacepackets/pipeline/pr-main This commit looks good
Rust/spacepackets/pipeline/head This commit looks good
2023-11-24 16:34:06 +01:00
4e90bbdc04 fix doctest
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-11-24 16:19:59 +01:00
48c9b12ee2 changelog fix
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-11-24 16:16:30 +01:00
b8d6cf9d85 improvements for writable abstractions
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-11-24 16:15:46 +01:00
9e74266b76 Merge pull request 'introduce new writable PDU packet abstraction' (#38) from writable_pdu_packet into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #38
2023-11-12 17:48:35 +01:00
e6408f74c1 new trait is public now
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-11-11 18:48:48 +01:00
18d650316c introduce new writable PDU packet abstraction
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-11-11 18:17:41 +01:00
016e0d8673 Merge pull request 'clippy fix' (#37) from clippy-fix into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #37
2023-10-10 10:03:17 +02:00
8cbfef4a1c clippy fix
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-10-10 10:00:14 +02:00
57c1d037df Merge pull request 'set direction field correctly' (#36) from pdu-set-direction-field-correctly into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #36
2023-10-06 15:04:17 +02:00
422b0107e5 changelog
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-10-06 15:02:12 +02:00
6c201206cc set direction field correctly
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-10-06 14:54:11 +02:00
5ebf0a5f4c Merge pull request 'update release checklist' (#35) from small-tweak into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #35
2023-09-26 18:15:32 +02:00
f1cf3802b5 update release checklist
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-09-26 18:15:05 +02:00
419acb19c4 Merge pull request 'oopsie' (#34) from oopsie into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #34
2023-09-26 17:17:22 +02:00
686b8eaaec oopsie
Some checks are pending
Rust/spacepackets/pipeline/head Build started...
2023-09-26 17:17:01 +02:00
34b58fe9cc Merge pull request 'bump version specifier' (#33) from prep_v0.7.0-beta2 into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #33
2023-09-26 17:15:29 +02:00
393c73cedf re-enable failing docs builds
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-09-26 17:11:39 +02:00
3e97bf0c15 bump version specifier
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-09-26 17:10:21 +02:00
7839fb3776 Merge pull request 'use non-deprecated API' (#32) from test-tweaks into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #32
2023-09-26 17:09:24 +02:00
55ad24db34 use non-deprecated API
Some checks are pending
Rust/spacepackets/pipeline/head Build started...
2023-09-26 17:08:58 +02:00
3b4a909ce1 Merge pull request 'Added to_vec method for SerializablePusPacket' (#31) from serializable-pus-to-vec into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #31
2023-09-26 16:59:41 +02:00
76ad1c7ead Added to_vec method for SerializablePusPacket
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-09-26 16:55:07 +02:00
79d26e1a67 Merge pull request 'Packet ID trait implementations' (#30) from packet-id-trait-impls into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #30
2023-09-18 18:19:31 +02:00
be37c15478 docs failure should not fail the whole build
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-09-18 18:00:14 +02:00
a6bced7983 this is okay
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-09-18 17:50:50 +02:00
5d8b5ce370 please stop
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-09-18 17:42:10 +02:00
b94d07f6c9 try 2
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-09-18 17:38:56 +02:00
90e48483bb next try
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-09-18 17:36:10 +02:00
963b9dbb5f inline PacketId raw call
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-09-18 17:34:49 +02:00
2a0db6b21c maybe this fixes the issue? 2023-09-18 17:34:04 +02:00
a4b14250c2 add stage to display toolchain info
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-09-18 17:32:54 +02:00
6116cdb27c add some tests
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-09-18 17:13:22 +02:00
6ebdf7e330 added packet ID trait impls
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-09-18 16:59:38 +02:00
e935b3825a Merge pull request 'release-checklist: missing bullet point' (#29) from release-checklist-note into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #29
2023-08-28 18:56:55 +02:00
a49737fc34 release-checklist: missing bullet point
Some checks are pending
Rust/spacepackets/pipeline/head Build queued...
2023-08-28 18:56:08 +02:00
3081539bb2 Merge pull request 'prep next beta release' (#28) from prep_v0.7.0-beta.1 into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #28
2023-08-28 18:54:32 +02:00
1b01c8bb0b small changelog note
Some checks are pending
Rust/spacepackets/pipeline/head Build started...
Rust/spacepackets/pipeline/pr-main Build started...
2023-08-28 18:54:01 +02:00
2ee3eee32e only allow zerocopy v0.7.0
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-08-28 18:47:41 +02:00
406d731bbe fix zerocopy usage
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-08-28 18:45:24 +02:00
49b50ec682 prep next beta release
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-08-28 18:36:00 +02:00
00fdfde015 Merge pull request 'invalid time code struct variant' (#26) from invalid-time-code-struct-variant into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #26
2023-08-28 17:42:46 +02:00
491b03c701 Merge remote-tracking branch 'origin/main' into invalid-time-code-struct-variant
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-08-28 17:32:10 +02:00
e090beedde CHANGELOG
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head This commit looks good
2023-08-28 17:31:41 +02:00
6f2ed3003f Merge pull request 'UnsignedByteFieldError: Use struct variants' (#27) from ubf-error-struct-variants into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #27
2023-08-28 17:27:34 +02:00
0b5a384743 fix tests
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-08-28 17:24:54 +02:00
925b2aa8d8 remove obsolete comment
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-08-28 17:22:36 +02:00
d98d4b55c8 convert UnsigedByteFieldError variants to struct variants
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-08-28 17:21:48 +02:00
94c378fa3b Merge branch 'main' into invalid-time-code-struct-variant
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-08-28 17:11:32 +02:00
1fc15230fa Merge pull request 'Size missmatch struct variant' (#25) from size-missmatch-struct-variant into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #25
2023-08-28 17:10:57 +02:00
e78f196a42 invalid time code struct variant
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-08-28 17:10:45 +02:00
2a11359a81 Merge branch 'main' into size-missmatch-struct-variant
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-08-28 17:01:14 +02:00
c226c5ea0f changelog
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-08-28 17:00:29 +02:00
ab65845573 Merge pull request 'PDU improvements and additions' (#24) from pdu-additions into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #24
2023-08-28 16:52:25 +02:00
3206af690c well that was a lot
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-08-18 10:09:32 +02:00
805065a7b9 cargo fmt
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-08-17 22:13:00 +02:00
62533bb91c Merge remote-tracking branch 'origin/main' into pdu-additions
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-08-17 21:34:38 +02:00
c085f9ab32 Merge pull request 'update LV and TLV code' (#22) from update-lv-tlv into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #22
2023-08-17 21:33:48 +02:00
f208a9b0f0 fixed a test
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-08-17 21:24:12 +02:00
c96a86a994 this should be better 2023-08-17 21:22:49 +02:00
9dfc593d2a fixes for pdu error enum 2023-08-17 21:04:27 +02:00
990b8de519 changelog
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-08-17 20:42:45 +02:00
965541e422 getter function for datafield len 2023-08-17 20:41:45 +02:00
9a52066314 fmt
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-08-16 18:19:41 +02:00
6ab05e2d83 fix for docs
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-08-16 18:19:15 +02:00
2d81a79321 Merge remote-tracking branch 'origin/main' into update-lv-tlv
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-08-16 18:17:51 +02:00
fc7bee342c Merge pull request 'Const UBF' (#23) from const-ubf into main
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Reviewed-on: #23
2023-08-16 18:17:13 +02:00
1789cff2b8 why is this still not a test
Some checks are pending
Rust/spacepackets/pipeline/head Build queued...
2023-08-16 18:16:43 +02:00
5ae5abe09a make UnsignedByteField helpers const
Some checks are pending
Rust/spacepackets/pipeline/head Build started...
2023-08-16 18:15:49 +02:00
407d1e1154 additional test for new method
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-08-16 18:10:39 +02:00
3ba575aac1 changelog
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-08-16 18:01:54 +02:00
81db36d159 additional docs
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-08-16 18:01:09 +02:00
081f6e840f added additional API
Some checks failed
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
Rust/spacepackets/pipeline/head There was a failure building this commit
2023-08-16 17:58:19 +02:00
3cb19298c8 some restructuring
Some checks failed
Rust/spacepackets/pipeline/head There was a failure building this commit
Rust/spacepackets/pipeline/pr-main There was a failure building this commit
2023-08-16 17:27:02 +02:00
4e2c0f1aa7 added a few additional tests
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
Rust/spacepackets/pipeline/pr-main This commit looks good
2023-08-16 16:36:15 +02:00
83db710950 update LV and TLV code
All checks were successful
Rust/spacepackets/pipeline/head This commit looks good
2023-08-16 16:27:10 +02:00
33 changed files with 9410 additions and 4112 deletions

View File

@ -1,42 +1,39 @@
on: [push]
name: ci
on: [push, pull_request]
jobs:
check:
name: Check
name: Check build
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- uses: actions-rs/cargo@v1
with:
command: check
args: --release
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo check --release
msrv:
name: Check with MSRV
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.61.0
override: true
profile: minimal
- uses: actions-rs/cargo@v1
with:
command: check
args: --release
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install nextest
uses: taiki-e/install-action@nextest
- run: cargo nextest run --all-features
- run: cargo test --doc
msrv:
name: Check MSRV
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.81.0
- run: cargo check --release
cross-check:
name: Check Cross
name: Check Cross-Compilation
runs-on: ubuntu-latest
strategy:
matrix:
@ -44,70 +41,32 @@ jobs:
- armv7-unknown-linux-gnueabihf
- thumbv7em-none-eabihf
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
profile: minimal
toolchain: stable
target: ${{ matrix.target }}
override: true
- uses: actions-rs/cargo@v1
with:
use-cross: true
command: check
args: --release --target=${{ matrix.target }} --no-default-features
targets: "armv7-unknown-linux-gnueabihf, thumbv7em-none-eabihf"
- run: cargo check --release --target=${{matrix.target}} --no-default-features
fmt:
name: Rustfmt
name: Check formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo fmt --all -- --check
check-doc:
docs:
name: Check Documentation Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
profile: minimal
- uses: actions-rs/cargo@v1
with:
command: doc
args: --all-features
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- run: RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options" cargo +nightly doc --all-features
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- run: rustup component add clippy
- uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings
ci:
if: ${{ success() }}
# all new jobs must be added to this list
needs: [check, fmt, clippy]
runs-on: ubuntu-latest
steps:
- name: CI succeeded
run: exit 0
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo clippy -- -D warnings

View File

@ -8,6 +8,280 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
# [unreleased]
- Bumped MSRV to 1.81.0
- Bump `zerocopy` to v0.8.0
- Bump `thiserror` to v2.0.0
# [v0.12.0] 2024-09-10
- Bumped MSRV to 1.70.0
## Added
- Added new `cfdp::tlv::TlvOwned` type which erases the lifetime and is clonable.
- Dedicated `cfdp::tlv::TlvLvDataTooLarge` error struct for APIs where this is the only possible
API error.
- Added File Data PDU API which expects the expected file data size and then exposes the unwritten
file data field as a mutable slice. This allows to read data from the virtual file system
API to the file data buffer without an intermediate buffer.
- Generic `EofPdu::new` constructor.
- Added generic sequence counter module.
- Added `MsgToUserTlv::to_tlv` converter which reduced the type and converts
it to a generic `Tlv`.
- Implemented `From<MsgToUserTlv> for Tlv` converter trait.
- Added CFDP maximum file segment length calculator method `calculate_max_file_seg_len_for_max_packet_len_and_pdu_header`
## Added and Changed
- Added new `ReadableTlv` to avoid some boilerplate code and have a common abstraction implemented
for both `Tlv` and `TlvOwned` to read the raw TLV data field and its length.
- Replaced `cfdp::tlv::TlvLvError` by `cfdp::tlv::TlvLvDataTooLarge` where applicable.
## Fixed
- Fixed an error in the EOF writer which wrote the fault location to the wrong buffer position.
- cfdp `ConditionCode::CheckLimitReached` previous had the wrong numerical value of `0b1001` (9)
and now has the correct value of `0b1010` (10).
## Changed
- Minor documentation build updates.
- Increased delegate version range to v0.13
# [v0.11.2] 2024-05-19
- Bumped MSRV to 1.68.2
## Fixed
- Removed `defmt::Format` impl for `MetadataPduCreator` which seems to be problematic.
# [v0.11.1] 2024-04-22
## Fixed
- The default data length for for `SpHeader` constructors where the data field length is not
specified is now 0.
- The `SpHeader::new_from_fields` is public now.
## Added
- `SpHeader::to_vec` method.
# [v0.11.0] 2024-04-16
## Changed
- Moved `CCSDS_HEADER_LEN` constant to the crate root.
## Added
- Added `SpacePacketHeader` type alias for `SpHeader` type.
# [v0.11.0-rc.2] 2024-04-04
## Changed
- Renamed `PacketId` and `PacketSequenceCtrl` `new` method to `new_checked` and former
`new_const` method to `new`.
- Renamed `tc`, `tm`, `tc_unseg` and `tm_unseg` variants for `PacketId` and `SpHeader`
to `new_for_tc_checked`, `new_for_tm_checked`, `new_for_unseg_tc_checked` and
`new_for_unseg_tm_checked`.
- `PusTmCreator` and `PusTcCreator` now expect a regular instance of `SpHeader` instead of
a mutable reference.
## Added
- `SpHeader::new_from_apid` and `SpHeader::new_from_apid_checked` constructor.
- `#[inline]` attribute for a lot of small functions.
# [v0.11.0-rc.1] 2024-04-03
Major API changes for the time API. If you are using the time API, it is strongly recommended
to check all the API changes in the **Changed** chapter.
## Fixed
- CUC timestamp was fixed to include leap second corrections because it is based on the TAI
time reference. The default CUC time object do not implement `CcsdsTimeProvider` anymore
because the trait methods require cached leap second information. This task is now performed
by the `cuc::CucTimeWithLeapSecs` which implements the trait.
## Added
- `From<$EcssEnum$TY> from $TY` for the ECSS enum type definitions.
- Added basic support conversions to the `time` library. Introduce new `chrono` and `timelib`
feature gate.
- Added `CcsdsTimeProvider::timelib_date_time`.
- Optional support for `defmt` by adding optional `defmt::Format` derives for common types.
## Changed
- `PusTcCreator::new_simple` now expects a valid slice for the source data instead of an optional
slice. For telecommands without application data, `&[]` can be passed.
- `PusTmSecondaryHeader` constructors now expects a valid slice for the time stamp instead of an
optional slice.
- Renamed `CcsdsTimeProvider::date_time` to `CcsdsTimeProvider::chrono_date_time`
- Renamed `CcsdsTimeCodes` to `CcsdsTimeCode`
- Renamed `cds::TimeProvider` to `cds::CdsTime`
- Renamed `cuc::TimeProviderCcsdsEpoch` to `cuc::CucTime`
- `UnixTimestamp` renamed to `UnixTime`
- `UnixTime` seconds are now private and can be retrieved using the `secs` member method.
- `UnixTime::new` renamed to `UnixTime::new_checked`.
- `UnixTime::secs` renamed to `UnixTime::as_secs`.
- `UnixTime` now has a nanosecond subsecond precision. The `new` constructor now expects
nanoseconds as the second argument.
- Added new `UnixTime::new_subsec_millis` and `UnixTime::new_subsec_millis_checked` API
to still allow creating a timestamp with only millisecond subsecond resolution.
- `CcsdsTimeProvider` now has a new `subsec_nanos` method in addition to a default
implementation for the `subsec_millis` method.
- `CcsdsTimeProvider::date_time` renamed to `CcsdsTimeProvider::chrono_date_time`.
- Added `UnixTime::MIN`, `UnixTime::MAX` and `UnixTime::EPOCH`.
- Added `UnixTime::timelib_date_time`.
- Error handling for ECSS and time module is more granular now, with a new
`DateBeforeCcsdsEpochError` error and a `DateBeforeCcsdsEpoch` enum variant for both
`CdsError` and `CucError`.
- `PusTmCreator` now has two lifetimes: One for the raw source data buffer and one for the
raw timestamp.
- Time API `from_now*` API renamed to `now*`.
## Removed
- Legacy `PusTm` and `PusTc` objects.
# [v0.11.0-rc.0] 2024-03-04
## Added
- `From<$TY>` for the `EcssEnum$TY` ECSS enum type definitions.
- `Sub` implementation for `UnixTimestamp` to calculate the duration between two timestamps.
## Changed
- `CcsdsTimeProvider` `subsecond_millis` function now returns `u16` instead of `Option<u16>`.
- `UnixTimestamp` `subsecond_millis` function now returns `u16` instead of `Option<u16>`.
# [v0.10.0] 2024-02-17
## Added
- Added `value` and `to_vec` methods for the `UnsignedEnum` trait. The value is returned as
as `u64`. Renamed former `value` method on `GenericUnsignedByteField` to `value_typed`.
- Added `value_const` const function for `UnsignedByteField` type.
- Added `value_typed` const functions for `GenericUnsignedByteField` and `GenericEcssEnumWrapper`.
# [v0.9.0] 2024-02-07
## Added
- `CcsdsPacket`, `PusPacket` and `GenericPusTmSecondaryHeader` implementation for
`PusTmZeroCopyWriter`.
- Additional length checks for `PusTmZeroCopyWriter`.
## Changed
- `PusTmZeroCopyWriter`: Added additional timestamp length argument for `new` constructor.
## Fixed
- Typo: `PUC_TM_MIN_HEADER_LEN` -> `PUS_TM_MIN_HEADER_LEN`
# [v0.8.1] 2024-02-05
## Fixed
- Added `pub` visibility for `PacketSequenceCtrl::const_new`.
# [v0.8.0] 2024-02-05
## Added
- Added `len_written` and `to_vec` methods to the `TimeWriter` trait.
# [v0.7.0] 2024-02-01
# [v0.7.0-beta.4] 2024-01-23
## Fixed
- `MetadataPduCreator`: The serialization function shifted the closure requested information
to the wrong position (first reserved bit) inside the raw content field.
# [v0.7.0-beta.3] 2023-12-06
## Added
- Add `WritablePduPacket` trait which is a common trait of all CFDP PDU implementations.
- Add `CfdpPdu` trait which exposes fields and attributes common to all CFDP PDUs.
- Add `GenericTlv` and `WritableTlv` trait as abstractions for the various TLV types.
## Fixed
- Set the direction field inside the PDU header field correctly explicitely for all CFDP PDU
packets.
## Changed
- Split up `FinishedPdu`into `FinishedPduCreator` and `FinishedPduReader` to expose specialized
APIs.
- Split up `MetadataPdu`into `MetadataPduCreator` and `MetadataPduReader` to expose specialized
APIs.
- Cleaned up CUC time implementation. Added `width` and `counter` getter methods.
- Renamed `SerializablePusPacket` to `WritablePusPacket`.
- Renamed `UnsignedPfc` to `PfcUnsigned` and `RealPfc` to `PfcReal`.
- Renamed `WritablePduPacket.written_len` and `SerializablePusPacket.len_packed` to `len_written`.
- Introduce custom implementation of `PartialEq` for `CommonPduConfig` which only compares the
values for the source entity ID, destination entity ID and transaction sequence number field to
allow those fields to have different widths.
- Removed the `PusError::RawDataTooShort` variant which is already covered by
`PusError::ByteConversionError` variant.
- Ranamed `TlvLvError::ByteConversionError` to `TlvLvError::ByteConversion`.
- Renamed `PusError::IncorrectCrc` to `PusError::ChecksumFailure`.
- Some more struct variant changes for error enumerations.
## Removed
- `PusError::NoRawData` variant.
- `cfdp::LenInBytes` which was not used.
# [v0.7.0-beta.2] 2023-09-26
## Added
- `PacketId` trait impls: `Ord`, `PartialOrd` and `Hash`
- `SerializablePusPacket` trait: Add `to_vec` method with default implementation.
# [v0.7.0-beta.1] 2023-08-28
- Bump `zerocopy` dependency to v0.7.0
## Changed
- The `Tlv` and `Lv` API return `&[u8]` instead of `Option<&[u8]>`.
- `ByteConversionError` error variants `ToSliceTooSmall` and `FromSliceTooSmall` are struct
variants now. `SizeMissmatch` was removed appropriately.
- `UnsignedByteFieldError` error variants `ValueTooLargeForWidth` and `InvalidWidth` are struct
variants now.
- `TimestampError` error variant `InvalidTimeCode` is struct variant now.
## Added
- Added `raw_data` API for `Tlv` and `Lv` to retrieve the whole `Lv`/`Tlv` slice if the object
was created from a raw bytestream.
- Added `MsgToUserTlv` helper class which wraps a regular `Tlv` and adds some useful functionality.
- `UnsignedByteField` and `GenericUnsignedByteField` `new` methods are `const` now.
- `PduError` variants which contained a tuple variant with multiple fields were converted to a
struct variant.
# Added
- Added `pdu_datafield_len` getter function for `PduHeader`
## Removed
- `SizeMissmatch` because it is not required for the `ByteConversionError` error enumeration
anymore.
# [v0.7.0-beta.0] 2023-08-16
- Moved MSRV from v1.60 to v1.61.
@ -60,7 +334,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
`PusTm` object.
## Changed
- The `EcssEnumeration` now requires the `UnsignedEnum` trait and only adds the `pfc` method to it.
- Renamed `byte_width` usages to `size` (part of new `UnsignedEnum` trait)
- Moved `ecss::CRC_CCITT_FALSE` CRC constant to the root module. This CRC type is not just used by

View File

@ -1,8 +1,8 @@
[package]
name = "spacepackets"
version = "0.7.0-beta.0"
version = "0.12.0"
edition = "2021"
rust-version = "1.61"
rust-version = "1.81.0"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
description = "Generic implementations for various CCSDS and ECSS packet standards"
homepage = "https://egit.irs.uni-stuttgart.de/rust/spacepackets"
@ -13,16 +13,20 @@ categories = ["aerospace", "aerospace::space-protocols", "no-std", "hardware-sup
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
zerocopy = "0.6"
crc = "3"
delegate = ">=0.8, <0.11"
delegate = ">=0.8, <=0.13"
paste = "1"
[dependencies.zerocopy]
version = "0.8"
features = ["derive"]
[dependencies.thiserror]
version = "1"
optional = true
version = "2"
default-features = false
[dependencies.num_enum]
version = "0.6"
version = ">0.5, <=0.7"
default-features = false
[dependencies.serde]
@ -31,23 +35,35 @@ optional = true
default-features = false
features = ["derive"]
[dependencies.time]
version = "0.3"
default-features = false
optional = true
[dependencies.chrono]
version = "0.4"
default-features = false
optional = true
[dependencies.num-traits]
version = "0.2"
default-features = false
[dev-dependencies.postcard]
version = "1"
[dependencies.defmt]
version = "0.3"
optional = true
[features]
default = ["std"]
std = ["chrono/std", "chrono/clock", "alloc", "thiserror"]
serde = ["dep:serde", "chrono/serde"]
alloc = ["postcard/alloc", "chrono/alloc"]
std = ["alloc", "chrono/std", "chrono/clock", "thiserror/std"]
serde = ["dep:serde", "chrono?/serde"]
alloc = ["chrono?/alloc", "defmt?/alloc", "serde?/alloc"]
timelib = ["dep:time"]
[dev-dependencies]
postcard = { version = "1", features = ["alloc"] }
chrono = "0.4"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "doc_cfg"]
rustdoc-args = ["--generate-link-to-definition"]

View File

@ -1,6 +1,7 @@
[![Crates.io](https://img.shields.io/crates/v/spacepackets)](https://crates.io/crates/spacepackets)
[![docs.rs](https://img.shields.io/docsrs/spacepackets)](https://docs.rs/spacepackets)
[![ci](https://github.com/us-irs/spacepackets-rs/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/us-irs/spacepackets-rs/actions/workflows/ci.yml)
[![coverage](https://shields.io/endpoint?url=https://absatsw.irs.uni-stuttgart.de/projects/spacepackets/coverage-rs/latest/coverage.json)](https://absatsw.irs.uni-stuttgart.de/projects/spacepackets/coverage-rs/latest/index.html)
ECSS and CCSDS Spacepackets
======
@ -28,10 +29,6 @@ Currently, this includes the following components:
`spacepackets` supports various runtime environments and is also suitable for `no_std` environments.
It also offers optional support for [`serde`](https://serde.rs/). This allows serializing and
deserializing them with an appropriate `serde` provider like
[`postcard`](https://github.com/jamesmunns/postcard).
## Default features
- [`std`](https://doc.rust-lang.org/std/): Enables functionality relying on the standard library.
@ -42,8 +39,35 @@ deserializing them with an appropriate `serde` provider like
## Optional Features
- [`serde`](https://serde.rs/): Adds `serde` support for most types by adding `Serialize` and `Deserialize` `derive`s
- [`chrono`](https://crates.io/crates/chrono): Add basic support for the `chrono` time library.
- [`timelib`](https://crates.io/crates/time): Add basic support for the `time` time library.
- [`defmt`](https://defmt.ferrous-systems.com/): Add support for the `defmt` by adding the
[`defmt::Format`](https://defmt.ferrous-systems.com/format) derive on many types.
# Examples
You can check the [documentation](https://docs.rs/spacepackets) of individual modules for various
usage examples.
# Coverage
Coverage was generated using [`grcov`](https://github.com/mozilla/grcov). If you have not done so
already, install the `llvm-tools-preview`:
```sh
rustup component add llvm-tools-preview
cargo install grcov --locked
```
After that, you can simply run `coverage.py` to test the project with coverage. You can optionally
supply the `--open` flag to open the coverage report in your webbrowser.
# Miri
You can run the [`miri`](https://github.com/rust-lang/miri) tool on this library to check for
undefined behaviour (UB). This library does not use use any `unsafe` code blocks, but `miri` could
still catch UB from used libraries.
```sh
cargo +nightly miri nextest run --all-features
```

View File

@ -6,10 +6,23 @@ RUN apt-get update
RUN apt-get --yes upgrade
# tzdata is a dependency, won't install otherwise
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get --yes install rsync curl
# set CROSS_CONTAINER_IN_CONTAINER to inform `cross` that it is executed from within a container
ENV CROSS_CONTAINER_IN_CONTAINER=true
RUN rustup install nightly && \
rustup target add thumbv7em-none-eabihf armv7-unknown-linux-gnueabihf && \
rustup component add rustfmt clippy
rustup component add rustfmt clippy llvm-tools-preview
# Get grcov
RUN curl -sSL https://github.com/mozilla/grcov/releases/download/v0.8.19/grcov-x86_64-unknown-linux-gnu.tar.bz2 | tar -xj --directory /usr/local/bin
# Get nextest
RUN curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin
# SSH stuff to allow deployment to doc server
RUN adduser --uid 114 jenkins
# Add documentation server to known hosts
RUN echo "|1|/LzCV4BuTmTb2wKnD146l9fTKgQ=|NJJtVjvWbtRt8OYqFgcYRnMQyVw= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNL8ssTonYtgiR/6RRlSIK9WU1ywOcJmxFTLcEblAwH7oifZzmYq3XRfwXrgfMpylEfMFYfCU8JRqtmi19xc21A=" >> /etc/ssh/ssh_known_hosts
RUN echo "|1|CcBvBc3EG03G+XM5rqRHs6gK/Gg=|oGeJQ+1I8NGI2THIkJsW92DpTzs= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNL8ssTonYtgiR/6RRlSIK9WU1ywOcJmxFTLcEblAwH7oifZzmYq3XRfwXrgfMpylEfMFYfCU8JRqtmi19xc21A=" >> /etc/ssh/ssh_known_hosts

119
automation/Jenkinsfile vendored
View File

@ -1,52 +1,81 @@
pipeline {
agent {
dockerfile {
dir 'automation'
reuseNode true
}
agent {
dockerfile {
dir 'automation'
reuseNode true
args '--network host'
}
}
stages {
stage('Clippy') {
steps {
sh 'cargo clippy'
}
}
stage('Docs') {
steps {
sh 'cargo +nightly doc --all-features'
}
}
stage('Rustfmt') {
steps {
sh 'cargo fmt --all --check'
}
}
stage('Test') {
steps {
sh 'cargo test --all-features'
}
}
stage('Check with all features') {
steps {
sh 'cargo check --all-features'
}
}
stage('Check with no features') {
steps {
sh 'cargo check --no-default-features'
}
}
stage('Check Cross Embedded Bare Metal') {
steps {
sh 'cargo check --target thumbv7em-none-eabihf --no-default-features'
}
}
stage('Check Cross Embedded Linux') {
steps {
sh 'cargo check --target armv7-unknown-linux-gnueabihf'
}
stages {
stage('Rust Toolchain Info') {
steps {
sh 'rustc --version'
}
}
stage('Clippy') {
steps {
sh 'cargo clippy'
}
}
stage('Docs') {
steps {
sh """
RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options" cargo +nightly doc --all-features
"""
}
}
stage('Rustfmt') {
steps {
sh 'cargo fmt --all --check'
}
}
stage('Test') {
steps {
sh 'cargo nextest r --all-features'
sh 'cargo test --doc'
}
}
stage('Check with all features') {
steps {
sh 'cargo check --all-features'
}
}
stage('Check with no features') {
steps {
sh 'cargo check --no-default-features'
}
}
stage('Check Cross Embedded Bare Metal') {
steps {
sh 'cargo check --target thumbv7em-none-eabihf --no-default-features'
}
}
stage('Check Cross Embedded Linux') {
steps {
sh 'cargo check --target armv7-unknown-linux-gnueabihf'
}
}
stage('Run test with Coverage') {
when {
anyOf {
branch 'main';
branch pattern: 'cov-deployment*'
}
}
steps {
withEnv(['RUSTFLAGS=-Cinstrument-coverage', 'LLVM_PROFILE_FILE=target/coverage/%p-%m.profraw']) {
echo "Executing tests with coverage"
sh 'cargo clean'
sh 'cargo test --all-features'
sh 'grcov . -s . --binary-path ./target/debug -t html --branch --ignore-not-existing -o ./target/debug/coverage/'
sshagent(credentials: ['documentation-buildfix']) {
// Deploy to Apache webserver
sh 'rsync --mkpath -r --delete ./target/debug/coverage/ buildfix@documentation.irs.uni-stuttgart.de:/projects/spacepackets/coverage-rs/latest/'
}
}
}
}
}
}

54
coverage.py Executable file
View File

@ -0,0 +1,54 @@
#!/usr/bin/env python3
import os
import logging
import argparse
import webbrowser
_LOGGER = logging.getLogger()
def generate_cov_report(open_report: bool, format: str):
logging.basicConfig(level=logging.INFO)
os.environ["RUSTFLAGS"] = "-Cinstrument-coverage"
os.environ["LLVM_PROFILE_FILE"] = "target/coverage/%p-%m.profraw"
_LOGGER.info("Executing tests with coverage")
os.system("cargo test --all-features")
out_path = "./target/debug/coverage"
if format == "lcov":
out_path = "./target/debug/lcov.info"
os.system(
f"grcov . -s . --binary-path ./target/debug/ -t {format} --branch --ignore-not-existing "
f"-o {out_path}"
)
if format == "lcov":
os.system(
"genhtml -o ./target/debug/coverage/ --show-details --highlight --ignore-errors source "
"--legend ./target/debug/lcov.info"
)
if open_report:
coverage_report_path = os.path.abspath("./target/debug/coverage/index.html")
webbrowser.open_new_tab(coverage_report_path)
_LOGGER.info("Done")
def main():
parser = argparse.ArgumentParser(
description="Generate coverage report and optionally open it in a browser"
)
parser.add_argument(
"--open", action="store_true", help="Open the coverage report in a browser"
)
parser.add_argument(
"--format",
choices=["html", "lcov"],
default="html",
help="Choose report format (html or lcov)",
)
args = parser.parse_args()
generate_cov_report(args.open, args.format)
if __name__ == "__main__":
main()

3
docs.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
export RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options"
cargo +nightly doc --all-features --open

View File

@ -4,15 +4,22 @@ Checklist for new releases
# Pre-Release
1. Make sure any new modules are documented sufficiently enough and check docs with
`cargo doc --all-features --open`.
`RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options" cargo +nightly doc --all-features --open`
or `cargo +nightly doc --all-features --config 'build.rustdocflags=["--cfg", "docsrs" --generate-link-to-definition"]' --open`
(was problematic on more recent nightly versions).
2. Bump version specifier in `Cargo.toml`.
3. Update `CHANGELOG.md`: Convert `unreleased` section into version section with date and add new
`unreleased` section.
4. Run `cargo test --all-features`.
4. Run `cargo test --all-features` or `cargo nextest r --all-features` together with
`cargo test --doc`.
5. Run `cargo fmt` and `cargo clippy`. Check `cargo msrv` against MSRV in `Cargo.toml`.
6. Wait for CI/CD results for EGit and Github. These also check cross-compilation for bare-metal
targets.
# Release
1. `cargo publish`
# Post-Release
1. Create a new release on `EGit` based on the release branch.

View File

@ -1,24 +1,38 @@
//! Generic CFDP length-value (LV) abstraction as specified in CFDP 5.1.8.
use crate::cfdp::TlvLvError;
use crate::{ByteConversionError, SizeMissmatch};
use crate::ByteConversionError;
use core::str::Utf8Error;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "std")]
use std::string::String;
use super::TlvLvDataTooLargeError;
pub const MIN_LV_LEN: usize = 1;
/// Generic CFDP length-value (LV) abstraction as specified in CFDP 5.1.8.
///
/// Please note that this class is zero-copy and does not generate a copy of the value data for
/// both the regular [Self::new] constructor and the [Self::from_bytes] constructor.
///
/// # Lifetimes
/// * `data`: If the LV is generated from a raw bytestream, this will be the lifetime of
/// the raw bytestream. If the LV is generated from a raw slice or a similar data reference,
/// this will be the lifetime of that data reference.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct Lv<'data> {
data: Option<&'data [u8]>,
data: &'data [u8],
// If the LV was generated from a raw bytestream, this will contain the start of the
// full LV.
pub(crate) raw_data: Option<&'data [u8]>,
}
impl PartialEq for Lv<'_> {
fn eq(&self, other: &Self) -> bool {
self.data == other.data
}
}
pub(crate) fn generic_len_check_data_serialization(
@ -27,10 +41,10 @@ pub(crate) fn generic_len_check_data_serialization(
min_overhead: usize,
) -> Result<(), ByteConversionError> {
if buf.len() < data_len + min_overhead {
return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: data_len + min_overhead,
}));
});
}
Ok(())
}
@ -40,68 +54,88 @@ pub(crate) fn generic_len_check_deserialization(
min_overhead: usize,
) -> Result<(), ByteConversionError> {
if buf.len() < min_overhead {
return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
return Err(ByteConversionError::FromSliceTooSmall {
found: buf.len(),
expected: min_overhead,
}));
});
}
Ok(())
}
impl<'data> Lv<'data> {
pub fn new(data: &[u8]) -> Result<Lv, TlvLvError> {
#[inline]
pub fn new(data: &[u8]) -> Result<Lv, TlvLvDataTooLargeError> {
if data.len() > u8::MAX as usize {
return Err(TlvLvError::DataTooLarge(data.len()));
return Err(TlvLvDataTooLargeError(data.len()));
}
Ok(Lv { data: Some(data) })
Ok(Lv {
data,
raw_data: None,
})
}
/// Creates a LV with an empty value field.
#[inline]
pub fn new_empty() -> Lv<'data> {
Lv { data: None }
Lv {
data: &[],
raw_data: None,
}
}
/// Helper function to build a string LV. This is especially useful for the file or directory
/// path LVs
pub fn new_from_str(str_slice: &str) -> Result<Lv, TlvLvError> {
#[inline]
pub fn new_from_str(str_slice: &str) -> Result<Lv, TlvLvDataTooLargeError> {
Self::new(str_slice.as_bytes())
}
/// Helper function to build a string LV. This is especially useful for the file or directory
/// path LVs
#[cfg(feature = "std")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))]
pub fn new_from_string(string: &'data String) -> Result<Lv<'data>, TlvLvError> {
#[inline]
pub fn new_from_string(string: &'data String) -> Result<Lv<'data>, TlvLvDataTooLargeError> {
Self::new(string.as_bytes())
}
/// Returns the length of the value part, not including the length byte.
#[inline]
pub fn len_value(&self) -> usize {
if self.data.is_none() {
return 0;
}
self.data.unwrap().len()
self.data.len()
}
/// Returns the full raw length, including the length byte.
#[inline]
pub fn len_full(&self) -> usize {
self.len_value() + 1
}
/// Checks whether the value field is empty.
#[inline]
pub fn is_empty(&self) -> bool {
self.data.is_none()
self.data.len() == 0
}
pub fn value(&self) -> Option<&[u8]> {
#[inline]
pub fn value(&self) -> &[u8] {
self.data
}
/// If the LV was generated from a raw bytestream using [Self::from_bytes], the raw start
/// of the LV can be retrieved with this method.
#[inline]
pub fn raw_data(&self) -> Option<&[u8]> {
self.raw_data
}
/// Convenience function to extract the value as a [str]. This is useful if the LV is
/// known to contain a [str], for example being a file name.
#[inline]
pub fn value_as_str(&self) -> Option<Result<&'data str, Utf8Error>> {
self.data?;
Some(core::str::from_utf8(self.data.unwrap()))
if self.is_empty() {
return None;
}
Some(core::str::from_utf8(self.data))
}
/// Writes the LV to a raw buffer. Please note that the first byte will contain the length
@ -112,40 +146,42 @@ impl<'data> Lv<'data> {
}
/// Reads a LV from a raw buffer.
#[inline]
pub fn from_bytes(buf: &'data [u8]) -> Result<Lv<'data>, ByteConversionError> {
generic_len_check_deserialization(buf, MIN_LV_LEN)?;
Self::from_be_bytes_no_len_check(buf)
}
pub(crate) fn write_to_be_bytes_no_len_check(&self, buf: &mut [u8]) -> usize {
if self.data.is_none() {
if self.is_empty() {
buf[0] = 0;
return MIN_LV_LEN;
}
let data = self.data.unwrap();
// Length check in constructor ensures the length always has a valid value.
buf[0] = data.len() as u8;
buf[MIN_LV_LEN..data.len() + MIN_LV_LEN].copy_from_slice(data);
MIN_LV_LEN + data.len()
buf[0] = self.data.len() as u8;
buf[MIN_LV_LEN..self.data.len() + MIN_LV_LEN].copy_from_slice(self.data);
MIN_LV_LEN + self.data.len()
}
#[inline]
pub(crate) fn from_be_bytes_no_len_check(
buf: &'data [u8],
) -> Result<Lv<'data>, ByteConversionError> {
let value_len = buf[0] as usize;
generic_len_check_deserialization(buf, value_len + MIN_LV_LEN)?;
let mut data = None;
if value_len > 0 {
data = Some(&buf[MIN_LV_LEN..MIN_LV_LEN + value_len])
}
Ok(Self { data })
Ok(Self {
data: &buf[MIN_LV_LEN..MIN_LV_LEN + value_len],
raw_data: Some(buf),
})
}
}
#[cfg(test)]
pub mod tests {
use crate::cfdp::lv::Lv;
use crate::cfdp::TlvLvError;
use alloc::string::ToString;
use super::*;
use crate::ByteConversionError;
use std::string::String;
@ -155,8 +191,8 @@ pub mod tests {
let lv_res = Lv::new(&lv_data);
assert!(lv_res.is_ok());
let lv = lv_res.unwrap();
assert!(lv.value().is_some());
let val = lv.value().unwrap();
assert!(!lv.value().is_empty());
let val = lv.value();
assert_eq!(val[0], 1);
assert_eq!(val[1], 2);
assert_eq!(val[2], 3);
@ -172,7 +208,6 @@ pub mod tests {
assert_eq!(lv_empty.len_value(), 0);
assert_eq!(lv_empty.len_full(), 1);
assert!(lv_empty.is_empty());
assert_eq!(lv_empty.value(), None);
let mut buf: [u8; 4] = [0xff; 4];
let res = lv_empty.write_to_be_bytes(&mut buf);
assert!(res.is_ok());
@ -211,10 +246,11 @@ pub mod tests {
assert!(lv.is_ok());
let lv = lv.unwrap();
assert!(!lv.is_empty());
assert!(lv.value().is_some());
assert_eq!(lv.len_value(), 4);
assert_eq!(lv.len_full(), 5);
let val = lv.value().unwrap();
assert!(lv.raw_data().is_some());
assert_eq!(lv.raw_data().unwrap(), buf);
let val = lv.value();
assert_eq!(val[0], 1);
assert_eq!(val[1], 2);
assert_eq!(val[2], 3);
@ -228,7 +264,6 @@ pub mod tests {
assert!(lv_empty.is_ok());
let lv_empty = lv_empty.unwrap();
assert!(lv_empty.is_empty());
assert!(lv_empty.value().is_none());
}
#[test]
@ -237,11 +272,11 @@ pub mod tests {
let lv = Lv::new(&data_big);
assert!(lv.is_err());
let error = lv.unwrap_err();
if let TlvLvError::DataTooLarge(size) = error {
assert_eq!(size, u8::MAX as usize + 1);
} else {
panic!("invalid exception {:?}", error)
}
assert_eq!(error.0, u8::MAX as usize + 1);
assert_eq!(
error.to_string(),
"data with size 256 larger than allowed 255 bytes"
);
}
#[test]
@ -252,9 +287,9 @@ pub mod tests {
let res = lv.write_to_be_bytes(&mut buf);
assert!(res.is_err());
let error = res.unwrap_err();
if let ByteConversionError::ToSliceTooSmall(missmatch) = error {
assert_eq!(missmatch.expected, 5);
assert_eq!(missmatch.found, 3);
if let ByteConversionError::ToSliceTooSmall { found, expected } = error {
assert_eq!(expected, 5);
assert_eq!(found, 3);
} else {
panic!("invalid error {}", error);
}
@ -267,9 +302,9 @@ pub mod tests {
let res = Lv::from_bytes(&buf);
assert!(res.is_err());
let error = res.unwrap_err();
if let ByteConversionError::FromSliceTooSmall(missmatch) = error {
assert_eq!(missmatch.found, 3);
assert_eq!(missmatch.expected, 5);
if let ByteConversionError::FromSliceTooSmall { found, expected } = error {
assert_eq!(found, 3);
assert_eq!(expected, 5);
} else {
panic!("invalid error {}", error);
}
@ -282,14 +317,14 @@ pub mod tests {
let res = res.unwrap();
assert_eq!(res, 8 + 1);
assert_eq!(buf[0], 8);
assert_eq!(buf[1], 't' as u8);
assert_eq!(buf[2], 'e' as u8);
assert_eq!(buf[3], 's' as u8);
assert_eq!(buf[4], 't' as u8);
assert_eq!(buf[5], '.' as u8);
assert_eq!(buf[6], 'b' as u8);
assert_eq!(buf[7], 'i' as u8);
assert_eq!(buf[8], 'n' as u8);
assert_eq!(buf[1], b't');
assert_eq!(buf[2], b'e');
assert_eq!(buf[3], b's');
assert_eq!(buf[4], b't');
assert_eq!(buf[5], b'.');
assert_eq!(buf[6], b'b');
assert_eq!(buf[7], b'i');
assert_eq!(buf[8], b'n');
}
#[test]
fn test_str_helper() {

View File

@ -1,11 +1,8 @@
//! Low-level CCSDS File Delivery Protocol (CFDP) support according to [CCSDS 727.0-B-5](https://public.ccsds.org/Pubs/727x0b5.pdf).
use crate::ByteConversionError;
use core::fmt::{Display, Formatter};
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "std")]
use std::error::Error;
pub mod lv;
pub mod pdu;
@ -18,6 +15,7 @@ pub const CFDP_VERSION_2: u8 = 0b001;
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum PduType {
FileDirective = 0,
@ -26,6 +24,7 @@ pub enum PduType {
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum Direction {
TowardsReceiver = 0,
@ -34,6 +33,7 @@ pub enum Direction {
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum TransmissionMode {
Acknowledged = 0,
@ -42,15 +42,35 @@ pub enum TransmissionMode {
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum CrcFlag {
NoCrc = 0,
WithCrc = 1,
}
impl From<bool> for CrcFlag {
fn from(value: bool) -> Self {
if value {
return CrcFlag::WithCrc;
}
CrcFlag::NoCrc
}
}
impl From<CrcFlag> for bool {
fn from(value: CrcFlag) -> Self {
if value == CrcFlag::WithCrc {
return true;
}
false
}
}
/// Always 0 and ignored for File Directive PDUs (CCSDS 727.0-B-5 P.75)
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum SegmentMetadataFlag {
NotPresent = 0,
@ -60,6 +80,7 @@ pub enum SegmentMetadataFlag {
/// Always 0 and ignored for File Directive PDUs (CCSDS 727.0-B-5 P.75)
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum SegmentationControl {
NoRecordBoundaryPreservation = 0,
@ -68,6 +89,7 @@ pub enum SegmentationControl {
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum FaultHandlerCode {
NoticeOfCancellation = 0b0001,
@ -78,17 +100,7 @@ pub enum FaultHandlerCode {
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(u8)]
pub enum LenInBytes {
ZeroOrNone = 0,
OneByte = 1,
TwoBytes = 2,
ThreeBytes = 4,
FourBytes = 8,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum ConditionCode {
/// This is not an error condition for which a faulty handler override can be specified
@ -101,7 +113,7 @@ pub enum ConditionCode {
FileSizeError = 0b0110,
NakLimitReached = 0b0111,
InactivityDetected = 0b1000,
CheckLimitReached = 0b1001,
CheckLimitReached = 0b1010,
UnsupportedChecksumType = 0b1011,
/// Not an actual fault condition for which fault handler overrides can be specified
SuspendRequestReceived = 0b1110,
@ -111,6 +123,7 @@ pub enum ConditionCode {
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum LargeFileFlag {
/// 32 bit maximum file size and FSS size
@ -119,10 +132,28 @@ pub enum LargeFileFlag {
Large = 1,
}
/// Transaction status for the ACK PDU field according to chapter 5.2.4 of the CFDP standard.
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum TransactionStatus {
/// Transaction is not currently active and the CFDP implementation does not retain a
/// transaction history.
Undefined = 0b00,
Active = 0b01,
/// Transaction was active in the past and was terminated.
Terminated = 0b10,
/// The CFDP implementation does retain a tranaction history, and the transaction is not and
/// never was active at this entity.
Unrecognized = 0b11,
}
/// Checksum types according to the
/// [SANA Checksum Types registry](https://sanaregistry.org/r/checksum_identifiers/)
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum ChecksumType {
/// Modular legacy checksum
@ -142,68 +173,96 @@ impl Default for ChecksumType {
pub const NULL_CHECKSUM_U32: [u8; 4] = [0; 4];
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[error("data with size {0} larger than allowed {max} bytes", max = u8::MAX)]
pub struct TlvLvDataTooLargeError(pub usize);
/// First value: Found value. Second value: Expected value if there is one.
#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[error("invalid TLV type field, found {found}, expected {expected:?}")]
pub struct InvalidTlvTypeFieldError {
found: u8,
expected: Option<u8>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum TlvLvError {
DataTooLarge(usize),
ByteConversionError(ByteConversionError),
/// First value: Found value. Second value: Expected value if there is one.
InvalidTlvTypeField((u8, Option<u8>)),
/// Logically invalid value length detected. The value length may not exceed 255 bytes.
/// Depending on the concrete TLV type, the value length may also be logically invalid.
#[error("{0}")]
DataTooLarge(#[from] TlvLvDataTooLargeError),
#[error("byte conversion error: {0}")]
ByteConversion(#[from] ByteConversionError),
#[error("{0}")]
InvalidTlvTypeField(#[from] InvalidTlvTypeFieldError),
#[error("invalid value length {0}")]
InvalidValueLength(usize),
/// Only applies to filestore requests and responses. Second name was missing where one is
/// expected.
#[error("second name missing for filestore request or response")]
SecondNameMissing,
/// Invalid action code for filestore requests or responses.
#[error("invalid action code {0}")]
InvalidFilestoreActionCode(u8),
}
impl From<ByteConversionError> for TlvLvError {
fn from(value: ByteConversionError) -> Self {
Self::ByteConversionError(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "serde")]
use crate::tests::generic_serde_test;
impl Display for TlvLvError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
TlvLvError::DataTooLarge(data_len) => {
write!(
f,
"data with size {} larger than allowed {} bytes",
data_len,
u8::MAX
)
}
TlvLvError::ByteConversionError(e) => {
write!(f, "{}", e)
}
TlvLvError::InvalidTlvTypeField((found, expected)) => {
write!(
f,
"invalid TLV type field, found {found}, possibly expected {expected:?}"
)
}
TlvLvError::InvalidValueLength(len) => {
write!(f, "invalid value length {len} detected")
}
TlvLvError::SecondNameMissing => {
write!(f, "second name missing for filestore request or response")
}
TlvLvError::InvalidFilestoreActionCode(raw) => {
write!(f, "invalid filestore action code with raw value {raw}")
}
}
#[test]
fn test_crc_from_bool() {
assert_eq!(CrcFlag::from(false), CrcFlag::NoCrc);
}
}
#[cfg(feature = "std")]
impl Error for TlvLvError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
TlvLvError::ByteConversionError(e) => Some(e),
_ => None,
}
#[test]
fn test_crc_flag_to_bool() {
let is_true: bool = CrcFlag::WithCrc.into();
assert!(is_true);
let is_false: bool = CrcFlag::NoCrc.into();
assert!(!is_false);
}
#[test]
fn test_default_checksum_type() {
let checksum = ChecksumType::default();
assert_eq!(checksum, ChecksumType::NullChecksum);
}
#[test]
fn test_fault_handler_code_from_u8() {
let fault_handler_code_raw = FaultHandlerCode::NoticeOfSuspension as u8;
let fault_handler_code = FaultHandlerCode::try_from(fault_handler_code_raw).unwrap();
assert_eq!(fault_handler_code, FaultHandlerCode::NoticeOfSuspension);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_impl_pdu_type() {
generic_serde_test(PduType::FileData);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_impl_direction() {
generic_serde_test(Direction::TowardsReceiver);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_impl_transmission_mode() {
generic_serde_test(TransmissionMode::Unacknowledged);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_fault_handler_code() {
generic_serde_test(FaultHandlerCode::NoticeOfCancellation);
}
}

335
src/cfdp/pdu/ack.rs Normal file
View File

@ -0,0 +1,335 @@
use crate::{
cfdp::{ConditionCode, CrcFlag, Direction, TransactionStatus},
ByteConversionError,
};
use super::{
add_pdu_crc, generic_length_checks_pdu_deserialization, CfdpPdu, FileDirectiveType, PduError,
PduHeader, WritablePduPacket,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// ACK PDU abstraction.
///
/// For more information, refer to CFDP chapter 5.2.4.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct AckPdu {
pdu_header: PduHeader,
directive_code_of_acked_pdu: FileDirectiveType,
condition_code: ConditionCode,
transaction_status: TransactionStatus,
}
impl AckPdu {
pub fn new(
mut pdu_header: PduHeader,
directive_code_of_acked_pdu: FileDirectiveType,
condition_code: ConditionCode,
transaction_status: TransactionStatus,
) -> Result<Self, PduError> {
if directive_code_of_acked_pdu == FileDirectiveType::EofPdu {
pdu_header.pdu_conf.direction = Direction::TowardsSender;
} else if directive_code_of_acked_pdu == FileDirectiveType::FinishedPdu {
pdu_header.pdu_conf.direction = Direction::TowardsReceiver;
} else {
return Err(PduError::InvalidDirectiveType {
found: directive_code_of_acked_pdu as u8,
expected: None,
});
}
// Force correct direction flag.
let mut ack_pdu = Self {
pdu_header,
directive_code_of_acked_pdu,
condition_code,
transaction_status,
};
ack_pdu.pdu_header.pdu_datafield_len = ack_pdu.calc_pdu_datafield_len() as u16;
Ok(ack_pdu)
}
pub fn new_for_eof_pdu(
pdu_header: PduHeader,
condition_code: ConditionCode,
transaction_status: TransactionStatus,
) -> Self {
// Unwrap okay here, [new] can only fail on invalid directive codes.
Self::new(
pdu_header,
FileDirectiveType::EofPdu,
condition_code,
transaction_status,
)
.unwrap()
}
pub fn new_for_finished_pdu(
pdu_header: PduHeader,
condition_code: ConditionCode,
transaction_status: TransactionStatus,
) -> Self {
// Unwrap okay here, [new] can only fail on invalid directive codes.
Self::new(
pdu_header,
FileDirectiveType::FinishedPdu,
condition_code,
transaction_status,
)
.unwrap()
}
pub fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
}
pub fn directive_code_of_acked_pdu(&self) -> FileDirectiveType {
self.directive_code_of_acked_pdu
}
pub fn condition_code(&self) -> ConditionCode {
self.condition_code
}
pub fn transaction_status(&self) -> TransactionStatus {
self.transaction_status
}
fn calc_pdu_datafield_len(&self) -> usize {
if self.crc_flag() == CrcFlag::WithCrc {
return 5;
}
3
}
pub fn from_bytes(buf: &[u8]) -> Result<AckPdu, PduError> {
let (pdu_header, mut current_idx) = PduHeader::from_bytes(buf)?;
let full_len_without_crc = pdu_header.verify_length_and_checksum(buf)?;
generic_length_checks_pdu_deserialization(buf, current_idx + 3, full_len_without_crc)?;
let directive_type = FileDirectiveType::try_from(buf[current_idx]).map_err(|_| {
PduError::InvalidDirectiveType {
found: buf[current_idx],
expected: Some(FileDirectiveType::AckPdu),
}
})?;
if directive_type != FileDirectiveType::AckPdu {
return Err(PduError::WrongDirectiveType {
found: directive_type,
expected: FileDirectiveType::AckPdu,
});
}
current_idx += 1;
let acked_directive_type =
FileDirectiveType::try_from(buf[current_idx] >> 4).map_err(|_| {
PduError::InvalidDirectiveType {
found: buf[current_idx],
expected: None,
}
})?;
if acked_directive_type != FileDirectiveType::EofPdu
&& acked_directive_type != FileDirectiveType::FinishedPdu
{
return Err(PduError::InvalidDirectiveType {
found: acked_directive_type as u8,
expected: None,
});
}
current_idx += 1;
let condition_code = ConditionCode::try_from((buf[current_idx] >> 4) & 0b1111)
.map_err(|_| PduError::InvalidConditionCode((buf[current_idx] >> 4) & 0b1111))?;
let transaction_status = TransactionStatus::try_from(buf[current_idx] & 0b11).unwrap();
Self::new(
pdu_header,
acked_directive_type,
condition_code,
transaction_status,
)
}
}
impl CfdpPdu for AckPdu {
fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
}
fn file_directive_type(&self) -> Option<FileDirectiveType> {
Some(FileDirectiveType::AckPdu)
}
}
impl WritablePduPacket for AckPdu {
fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, PduError> {
let expected_len = self.len_written();
if buf.len() < expected_len {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: expected_len,
}
.into());
}
let mut current_idx = self.pdu_header.write_to_bytes(buf)?;
buf[current_idx] = FileDirectiveType::AckPdu as u8;
current_idx += 1;
buf[current_idx] = (self.directive_code_of_acked_pdu as u8) << 4;
if self.directive_code_of_acked_pdu == FileDirectiveType::FinishedPdu {
// This is the directive subtype code. It needs to be set to 0b0001 if the ACK PDU
// acknowledges a Finished PDU, and to 0b0000 otherwise.
buf[current_idx] |= 0b0001;
}
current_idx += 1;
buf[current_idx] = ((self.condition_code as u8) << 4) | (self.transaction_status as u8);
current_idx += 1;
if self.crc_flag() == CrcFlag::WithCrc {
current_idx = add_pdu_crc(buf, current_idx);
}
Ok(current_idx)
}
fn len_written(&self) -> usize {
self.pdu_header.header_len() + self.calc_pdu_datafield_len()
}
}
#[cfg(test)]
mod tests {
use crate::cfdp::{
pdu::tests::{common_pdu_conf, verify_raw_header, TEST_DEST_ID, TEST_SEQ_NUM, TEST_SRC_ID},
LargeFileFlag, PduType, TransmissionMode,
};
use super::*;
#[cfg(feature = "serde")]
use crate::tests::generic_serde_test;
fn verify_state(ack_pdu: &AckPdu, expected_crc_flag: CrcFlag, expected_dir: Direction) {
assert_eq!(ack_pdu.condition_code(), ConditionCode::NoError);
assert_eq!(ack_pdu.transaction_status(), TransactionStatus::Active);
assert_eq!(ack_pdu.crc_flag(), expected_crc_flag);
assert_eq!(ack_pdu.file_flag(), LargeFileFlag::Normal);
assert_eq!(ack_pdu.pdu_type(), PduType::FileDirective);
assert_eq!(
ack_pdu.file_directive_type(),
Some(FileDirectiveType::AckPdu)
);
assert_eq!(ack_pdu.transmission_mode(), TransmissionMode::Acknowledged);
assert_eq!(ack_pdu.direction(), expected_dir);
assert_eq!(ack_pdu.source_id(), TEST_SRC_ID.into());
assert_eq!(ack_pdu.dest_id(), TEST_DEST_ID.into());
assert_eq!(ack_pdu.transaction_seq_num(), TEST_SEQ_NUM.into());
}
#[test]
fn test_basic() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let ack_pdu = AckPdu::new(
pdu_header,
FileDirectiveType::FinishedPdu,
ConditionCode::NoError,
TransactionStatus::Active,
)
.expect("creating ACK PDU failed");
assert_eq!(
ack_pdu.directive_code_of_acked_pdu(),
FileDirectiveType::FinishedPdu
);
verify_state(&ack_pdu, CrcFlag::NoCrc, Direction::TowardsReceiver);
}
fn generic_serialization_test(
condition_code: ConditionCode,
transaction_status: TransactionStatus,
) {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let ack_pdu = AckPdu::new_for_finished_pdu(pdu_header, condition_code, transaction_status);
let mut buf: [u8; 64] = [0; 64];
let res = ack_pdu.write_to_bytes(&mut buf);
assert!(res.is_ok());
let written = res.unwrap();
assert_eq!(written, ack_pdu.len_written());
verify_raw_header(ack_pdu.pdu_header(), &buf);
assert_eq!(buf[7], FileDirectiveType::AckPdu as u8);
assert_eq!((buf[8] >> 4) & 0b1111, FileDirectiveType::FinishedPdu as u8);
assert_eq!(buf[8] & 0b1111, 0b0001);
assert_eq!(buf[9] >> 4 & 0b1111, condition_code as u8);
assert_eq!(buf[9] & 0b11, transaction_status as u8);
assert_eq!(written, 10);
}
#[test]
fn test_serialization_no_error() {
generic_serialization_test(ConditionCode::NoError, TransactionStatus::Active);
}
#[test]
fn test_serialization_fs_error() {
generic_serialization_test(ConditionCode::FileSizeError, TransactionStatus::Terminated);
}
#[test]
fn test_deserialization() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let ack_pdu = AckPdu::new_for_finished_pdu(
pdu_header,
ConditionCode::NoError,
TransactionStatus::Active,
);
let ack_vec = ack_pdu.to_vec().unwrap();
let ack_deserialized =
AckPdu::from_bytes(&ack_vec).expect("ACK PDU deserialization failed");
assert_eq!(ack_deserialized, ack_pdu);
}
#[test]
fn test_with_crc() {
let pdu_conf = common_pdu_conf(CrcFlag::WithCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let ack_pdu = AckPdu::new_for_finished_pdu(
pdu_header,
ConditionCode::NoError,
TransactionStatus::Active,
);
let ack_vec = ack_pdu.to_vec().unwrap();
assert_eq!(ack_vec.len(), ack_pdu.len_written());
assert_eq!(ack_vec.len(), 12);
let ack_deserialized =
AckPdu::from_bytes(&ack_vec).expect("ACK PDU deserialization failed");
assert_eq!(ack_deserialized, ack_pdu);
}
#[test]
fn test_for_eof_pdu() {
let pdu_conf = common_pdu_conf(CrcFlag::WithCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let ack_pdu = AckPdu::new_for_eof_pdu(
pdu_header,
ConditionCode::NoError,
TransactionStatus::Active,
);
assert_eq!(
ack_pdu.directive_code_of_acked_pdu(),
FileDirectiveType::EofPdu
);
verify_state(&ack_pdu, CrcFlag::WithCrc, Direction::TowardsSender);
}
#[test]
#[cfg(feature = "serde")]
fn test_ack_pdu_serialization() {
let pdu_conf = common_pdu_conf(CrcFlag::WithCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let ack_pdu = AckPdu::new_for_eof_pdu(
pdu_header,
ConditionCode::NoError,
TransactionStatus::Active,
);
generic_serde_test(ack_pdu);
}
}

View File

@ -2,17 +2,20 @@ use crate::cfdp::pdu::{
add_pdu_crc, generic_length_checks_pdu_deserialization, read_fss_field, write_fss_field,
FileDirectiveType, PduError, PduHeader,
};
use crate::cfdp::tlv::EntityIdTlv;
use crate::cfdp::{ConditionCode, CrcFlag, LargeFileFlag};
use crate::{ByteConversionError, SizeMissmatch};
use crate::cfdp::tlv::{EntityIdTlv, WritableTlv};
use crate::cfdp::{ConditionCode, CrcFlag, Direction, LargeFileFlag};
use crate::ByteConversionError;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use super::{CfdpPdu, WritablePduPacket};
/// Finished PDU abstraction.
///
/// For more information, refer to CFDP chapter 5.2.2.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct EofPdu {
pdu_header: PduHeader,
condition_code: ConditionCode,
@ -22,24 +25,38 @@ pub struct EofPdu {
}
impl EofPdu {
pub fn new_no_error(pdu_header: PduHeader, file_checksum: u32, file_size: u64) -> Self {
pub fn new(
mut pdu_header: PduHeader,
condition_code: ConditionCode,
file_checksum: u32,
file_size: u64,
fault_location: Option<EntityIdTlv>,
) -> Self {
// Force correct direction flag.
pdu_header.pdu_conf.direction = Direction::TowardsReceiver;
let mut eof_pdu = Self {
pdu_header,
condition_code: ConditionCode::NoError,
condition_code,
file_checksum,
file_size,
fault_location: None,
fault_location,
};
eof_pdu.pdu_header.pdu_datafield_len = eof_pdu.calc_pdu_datafield_len() as u16;
eof_pdu
}
pub fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
pub fn new_no_error(pdu_header: PduHeader, file_checksum: u32, file_size: u64) -> Self {
Self::new(
pdu_header,
ConditionCode::NoError,
file_checksum,
file_size,
None,
)
}
pub fn written_len(&self) -> usize {
self.pdu_header.header_len() + self.calc_pdu_datafield_len()
pub fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
}
pub fn condition_code(&self) -> ConditionCode {
@ -63,39 +80,12 @@ impl EofPdu {
if let Some(fault_location) = self.fault_location {
len += fault_location.len_full();
}
if self.crc_flag() == CrcFlag::WithCrc {
len += 2;
}
len
}
pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, PduError> {
let expected_len = self.written_len();
if buf.len() < expected_len {
return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
found: buf.len(),
expected: expected_len,
})
.into());
}
let mut current_idx = self.pdu_header.write_to_bytes(buf)?;
buf[current_idx] = FileDirectiveType::EofPdu as u8;
current_idx += 1;
buf[current_idx] = (self.condition_code as u8) << 4;
current_idx += 1;
buf[current_idx..current_idx + 4].copy_from_slice(&self.file_checksum.to_be_bytes());
current_idx += 4;
current_idx += write_fss_field(
self.pdu_header.pdu_conf.file_flag,
self.file_size,
&mut buf[current_idx..],
)?;
if let Some(fault_location) = self.fault_location {
current_idx += fault_location.write_to_be_bytes(buf)?;
}
if self.pdu_header.pdu_conf.crc_flag == CrcFlag::WithCrc {
current_idx = add_pdu_crc(buf, current_idx);
}
Ok(current_idx)
}
pub fn from_bytes(buf: &[u8]) -> Result<EofPdu, PduError> {
let (pdu_header, mut current_idx) = PduHeader::from_bytes(buf)?;
let full_len_without_crc = pdu_header.verify_length_and_checksum(buf)?;
@ -106,13 +96,16 @@ impl EofPdu {
}
generic_length_checks_pdu_deserialization(buf, min_expected_len, full_len_without_crc)?;
let directive_type = FileDirectiveType::try_from(buf[current_idx]).map_err(|_| {
PduError::InvalidDirectiveType((buf[current_idx], FileDirectiveType::EofPdu))
PduError::InvalidDirectiveType {
found: buf[current_idx],
expected: Some(FileDirectiveType::EofPdu),
}
})?;
if directive_type != FileDirectiveType::EofPdu {
return Err(PduError::WrongDirectiveType((
directive_type,
FileDirectiveType::EofPdu,
)));
return Err(PduError::WrongDirectiveType {
found: directive_type,
expected: FileDirectiveType::EofPdu,
});
}
current_idx += 1;
let condition_code = ConditionCode::try_from((buf[current_idx] >> 4) & 0b1111)
@ -138,22 +131,99 @@ impl EofPdu {
}
}
impl CfdpPdu for EofPdu {
fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
}
fn file_directive_type(&self) -> Option<FileDirectiveType> {
Some(FileDirectiveType::EofPdu)
}
}
impl WritablePduPacket for EofPdu {
fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, PduError> {
let expected_len = self.len_written();
if buf.len() < expected_len {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: expected_len,
}
.into());
}
let mut current_idx = self.pdu_header.write_to_bytes(buf)?;
buf[current_idx] = FileDirectiveType::EofPdu as u8;
current_idx += 1;
buf[current_idx] = (self.condition_code as u8) << 4;
current_idx += 1;
buf[current_idx..current_idx + 4].copy_from_slice(&self.file_checksum.to_be_bytes());
current_idx += 4;
current_idx += write_fss_field(
self.pdu_header.pdu_conf.file_flag,
self.file_size,
&mut buf[current_idx..],
)?;
if let Some(fault_location) = self.fault_location {
current_idx += fault_location.write_to_bytes(&mut buf[current_idx..])?;
}
if self.crc_flag() == CrcFlag::WithCrc {
current_idx = add_pdu_crc(buf, current_idx);
}
Ok(current_idx)
}
fn len_written(&self) -> usize {
self.pdu_header.header_len() + self.calc_pdu_datafield_len()
}
}
#[cfg(test)]
mod tests {
use crate::cfdp::pdu::eof::EofPdu;
use crate::cfdp::pdu::tests::{common_pdu_conf, verify_raw_header};
use super::*;
use crate::cfdp::pdu::tests::{
common_pdu_conf, verify_raw_header, TEST_DEST_ID, TEST_SEQ_NUM, TEST_SRC_ID,
};
use crate::cfdp::pdu::{FileDirectiveType, PduHeader};
use crate::cfdp::{ConditionCode, CrcFlag, LargeFileFlag};
use crate::cfdp::{ConditionCode, CrcFlag, LargeFileFlag, PduType, TransmissionMode};
#[cfg(feature = "serde")]
use crate::tests::generic_serde_test;
use crate::util::{UnsignedByteFieldU16, UnsignedEnum};
fn verify_state_no_error_no_crc(eof_pdu: &EofPdu, file_flag: LargeFileFlag) {
verify_state(eof_pdu, CrcFlag::NoCrc, file_flag, ConditionCode::NoError);
}
fn verify_state(
eof_pdu: &EofPdu,
crc_flag: CrcFlag,
file_flag: LargeFileFlag,
cond_code: ConditionCode,
) {
assert_eq!(eof_pdu.file_checksum(), 0x01020304);
assert_eq!(eof_pdu.file_size(), 12);
assert_eq!(eof_pdu.condition_code(), cond_code);
assert_eq!(eof_pdu.crc_flag(), crc_flag);
assert_eq!(eof_pdu.file_flag(), file_flag);
assert_eq!(eof_pdu.pdu_type(), PduType::FileDirective);
assert_eq!(
eof_pdu.file_directive_type(),
Some(FileDirectiveType::EofPdu)
);
assert_eq!(eof_pdu.transmission_mode(), TransmissionMode::Acknowledged);
assert_eq!(eof_pdu.direction(), Direction::TowardsReceiver);
assert_eq!(eof_pdu.source_id(), TEST_SRC_ID.into());
assert_eq!(eof_pdu.dest_id(), TEST_DEST_ID.into());
assert_eq!(eof_pdu.transaction_seq_num(), TEST_SEQ_NUM.into());
}
#[test]
fn test_basic() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let eof_pdu = EofPdu::new_no_error(pdu_header, 0x01020304, 12);
assert_eq!(eof_pdu.written_len(), pdu_header.header_len() + 2 + 4 + 4);
assert_eq!(eof_pdu.file_checksum(), 0x01020304);
assert_eq!(eof_pdu.file_size(), 12);
assert_eq!(eof_pdu.condition_code(), ConditionCode::NoError);
assert_eq!(eof_pdu.len_written(), pdu_header.header_len() + 2 + 4 + 4);
verify_state_no_error_no_crc(&eof_pdu, LargeFileFlag::Normal);
}
#[test]
@ -165,7 +235,7 @@ mod tests {
let res = eof_pdu.write_to_bytes(&mut buf);
assert!(res.is_ok());
let written = res.unwrap();
assert_eq!(written, eof_pdu.written_len());
assert_eq!(written, eof_pdu.len_written());
verify_raw_header(eof_pdu.pdu_header(), &buf);
let mut current_idx = eof_pdu.pdu_header().header_len();
buf[current_idx] = FileDirectiveType::EofPdu as u8;
@ -196,11 +266,103 @@ mod tests {
let mut buf: [u8; 64] = [0; 64];
eof_pdu.write_to_bytes(&mut buf).unwrap();
let eof_read_back = EofPdu::from_bytes(&buf);
if !eof_read_back.is_ok() {
let e = eof_read_back.unwrap_err();
if let Err(e) = eof_read_back {
panic!("deserialization failed with: {e}")
}
let eof_read_back = eof_read_back.unwrap();
assert_eq!(eof_read_back, eof_pdu);
}
#[test]
fn test_write_to_vec() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let eof_pdu = EofPdu::new_no_error(pdu_header, 0x01020304, 12);
let mut buf: [u8; 64] = [0; 64];
let written = eof_pdu.write_to_bytes(&mut buf).unwrap();
let pdu_vec = eof_pdu.to_vec().unwrap();
assert_eq!(buf[0..written], pdu_vec);
}
#[test]
fn test_with_crc() {
let pdu_conf = common_pdu_conf(CrcFlag::WithCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let eof_pdu = EofPdu::new_no_error(pdu_header, 0x01020304, 12);
let mut buf: [u8; 64] = [0; 64];
let written = eof_pdu.write_to_bytes(&mut buf).unwrap();
assert_eq!(written, eof_pdu.len_written());
let eof_from_raw = EofPdu::from_bytes(&buf).expect("creating EOF PDU failed");
assert_eq!(eof_from_raw, eof_pdu);
buf[written - 1] -= 1;
let crc: u16 = ((buf[written - 2] as u16) << 8) as u16 | buf[written - 1] as u16;
let error = EofPdu::from_bytes(&buf).unwrap_err();
if let PduError::Checksum(e) = error {
assert_eq!(e, crc);
} else {
panic!("expected crc error");
}
}
#[test]
fn test_with_large_file_flag() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Large);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let eof_pdu = EofPdu::new_no_error(pdu_header, 0x01020304, 12);
verify_state_no_error_no_crc(&eof_pdu, LargeFileFlag::Large);
assert_eq!(eof_pdu.len_written(), pdu_header.header_len() + 2 + 8 + 4);
}
#[test]
#[cfg(feature = "serde")]
fn test_eof_serde() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let eof_pdu = EofPdu::new_no_error(pdu_header, 0x01020304, 12);
generic_serde_test(eof_pdu);
}
fn generic_test_with_fault_location_and_error(crc: CrcFlag) {
let pdu_conf = common_pdu_conf(crc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let eof_pdu = EofPdu::new(
pdu_header,
ConditionCode::FileChecksumFailure,
0x01020304,
12,
Some(EntityIdTlv::new(UnsignedByteFieldU16::new(5).into())),
);
let mut expected_len = pdu_header.header_len() + 2 + 4 + 4 + 4;
if crc == CrcFlag::WithCrc {
expected_len += 2;
}
// Entity ID TLV increaes length by 4.
assert_eq!(eof_pdu.len_written(), expected_len);
verify_state(
&eof_pdu,
crc,
LargeFileFlag::Normal,
ConditionCode::FileChecksumFailure,
);
let eof_vec = eof_pdu.to_vec().unwrap();
let eof_read_back = EofPdu::from_bytes(&eof_vec);
if let Err(e) = eof_read_back {
panic!("deserialization failed with: {e}")
}
let eof_read_back = eof_read_back.unwrap();
assert_eq!(eof_read_back, eof_pdu);
assert!(eof_read_back.fault_location.is_some());
assert_eq!(eof_read_back.fault_location.unwrap().entity_id().value(), 5);
assert_eq!(eof_read_back.fault_location.unwrap().entity_id().size(), 2);
}
#[test]
fn test_with_fault_location_and_error() {
generic_test_with_fault_location_and_error(CrcFlag::NoCrc);
}
#[test]
fn test_with_fault_location_and_error_and_crc() {
generic_test_with_fault_location_and_error(CrcFlag::WithCrc);
}
}

View File

@ -3,11 +3,13 @@ use crate::cfdp::pdu::{
PduError, PduHeader,
};
use crate::cfdp::{CrcFlag, LargeFileFlag, PduType, SegmentMetadataFlag};
use crate::{ByteConversionError, SizeMissmatch};
use crate::ByteConversionError;
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use super::{CfdpPdu, FileDirectiveType, WritablePduPacket};
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(u8)]
@ -41,6 +43,14 @@ impl<'seg_meta> SegmentMetadata<'seg_meta> {
})
}
pub fn record_continuation_state(&self) -> RecordContinuationState {
self.record_continuation_state
}
pub fn metadata(&self) -> Option<&'seg_meta [u8]> {
self.metadata
}
pub fn written_len(&self) -> usize {
// Map empty metadata to 0 and slice to its length.
1 + self.metadata.map_or(0, |meta| meta.len())
@ -48,10 +58,10 @@ impl<'seg_meta> SegmentMetadata<'seg_meta> {
pub(crate) fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
if buf.len() < self.written_len() {
return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: self.written_len(),
}));
});
}
buf[0] = ((self.record_continuation_state as u8) << 6)
| self.metadata.map_or(0, |meta| meta.len() as u8);
@ -63,10 +73,10 @@ impl<'seg_meta> SegmentMetadata<'seg_meta> {
pub(crate) fn from_bytes(buf: &'seg_meta [u8]) -> Result<Self, ByteConversionError> {
if buf.is_empty() {
return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
return Err(ByteConversionError::FromSliceTooSmall {
found: buf.len(),
expected: 2,
}));
});
}
let mut metadata = None;
let seg_metadata_len = (buf[0] & 0b111111) as usize;
@ -82,16 +92,67 @@ impl<'seg_meta> SegmentMetadata<'seg_meta> {
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
struct FdPduBase<'seg_meta> {
pdu_header: PduHeader,
#[cfg_attr(feature = "serde", serde(borrow))]
segment_metadata: Option<SegmentMetadata<'seg_meta>>,
offset: u64,
}
impl CfdpPdu for FdPduBase<'_> {
fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
}
fn file_directive_type(&self) -> Option<FileDirectiveType> {
None
}
}
impl FdPduBase<'_> {
fn calc_pdu_datafield_len(&self, file_data_len: u64) -> usize {
let mut len = core::mem::size_of::<u32>();
if self.pdu_header.pdu_conf.file_flag == LargeFileFlag::Large {
len += 4;
}
if self.segment_metadata.is_some() {
len += self.segment_metadata.as_ref().unwrap().written_len()
}
len += file_data_len as usize;
if self.crc_flag() == CrcFlag::WithCrc {
len += 2;
}
len
}
fn write_common_fields_to_bytes(&self, buf: &mut [u8]) -> Result<usize, PduError> {
let mut current_idx = self.pdu_header.write_to_bytes(buf)?;
if self.segment_metadata.is_some() {
current_idx += self
.segment_metadata
.as_ref()
.unwrap()
.write_to_bytes(&mut buf[current_idx..])?;
}
current_idx += write_fss_field(
self.pdu_header.common_pdu_conf().file_flag,
self.offset,
&mut buf[current_idx..],
)?;
Ok(current_idx)
}
}
/// File Data PDU abstraction.
///
/// For more information, refer to CFDP chapter 5.3.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FileDataPdu<'seg_meta, 'file_data> {
pdu_header: PduHeader,
#[cfg_attr(feature = "serde", serde(borrow))]
segment_metadata: Option<SegmentMetadata<'seg_meta>>,
offset: u64,
common: FdPduBase<'seg_meta>,
file_data: &'file_data [u8],
}
@ -124,77 +185,35 @@ impl<'seg_meta, 'file_data> FileDataPdu<'seg_meta, 'file_data> {
pdu_header.seg_metadata_flag = SegmentMetadataFlag::Present;
}
let mut pdu = Self {
pdu_header,
segment_metadata,
offset,
common: FdPduBase {
pdu_header,
segment_metadata,
offset,
},
file_data,
};
pdu.pdu_header.pdu_datafield_len = pdu.calc_pdu_datafield_len() as u16;
pdu.common.pdu_header.pdu_datafield_len = pdu.calc_pdu_datafield_len() as u16;
pdu
}
fn calc_pdu_datafield_len(&self) -> usize {
let mut len = core::mem::size_of::<u32>();
if self.pdu_header.pdu_conf.file_flag == LargeFileFlag::Large {
len += 4;
}
if self.segment_metadata.is_some() {
len += self.segment_metadata.as_ref().unwrap().written_len()
}
len += self.file_data.len();
if self.pdu_header.pdu_conf.crc_flag == CrcFlag::WithCrc {
len += 2;
}
len
self.common
.calc_pdu_datafield_len(self.file_data.len() as u64)
}
pub fn written_len(&self) -> usize {
self.pdu_header.header_len() + self.calc_pdu_datafield_len()
pub fn segment_metadata(&self) -> Option<&SegmentMetadata> {
self.common.segment_metadata.as_ref()
}
pub fn offset(&self) -> u64 {
self.offset
self.common.offset
}
pub fn file_data(&self) -> &'file_data [u8] {
self.file_data
}
pub fn segment_metadata(&self) -> Option<&SegmentMetadata> {
self.segment_metadata.as_ref()
}
pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, PduError> {
if buf.len() < self.written_len() {
return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
found: buf.len(),
expected: self.written_len(),
})
.into());
}
let mut current_idx = self.pdu_header.write_to_bytes(buf)?;
if self.segment_metadata.is_some() {
current_idx += self
.segment_metadata
.as_ref()
.unwrap()
.write_to_bytes(&mut buf[current_idx..])?;
}
current_idx += write_fss_field(
self.pdu_header.common_pdu_conf().file_flag,
self.offset,
&mut buf[current_idx..],
)?;
buf[current_idx..current_idx + self.file_data.len()].copy_from_slice(self.file_data);
current_idx += self.file_data.len();
if self.pdu_header.pdu_conf.crc_flag == CrcFlag::WithCrc {
current_idx = add_pdu_crc(buf, current_idx);
}
Ok(current_idx)
}
pub fn from_bytes<'longest: 'seg_meta + 'file_data>(
buf: &'longest [u8],
) -> Result<Self, PduError> {
pub fn from_bytes<'buf: 'seg_meta + 'file_data>(buf: &'buf [u8]) -> Result<Self, PduError> {
let (pdu_header, mut current_idx) = PduHeader::from_bytes(buf)?;
let full_len_without_crc = pdu_header.verify_length_and_checksum(buf)?;
let min_expected_len = current_idx + core::mem::size_of::<u32>();
@ -207,35 +226,226 @@ impl<'seg_meta, 'file_data> FileDataPdu<'seg_meta, 'file_data> {
let (fss, offset) = read_fss_field(pdu_header.pdu_conf.file_flag, &buf[current_idx..]);
current_idx += fss;
if current_idx > full_len_without_crc {
return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
return Err(ByteConversionError::FromSliceTooSmall {
found: current_idx,
expected: full_len_without_crc,
})
}
.into());
}
Ok(Self {
pdu_header,
segment_metadata,
offset,
common: FdPduBase {
pdu_header,
segment_metadata,
offset,
},
file_data: &buf[current_idx..full_len_without_crc],
})
}
}
impl CfdpPdu for FileDataPdu<'_, '_> {
fn pdu_header(&self) -> &PduHeader {
&self.common.pdu_header
}
fn file_directive_type(&self) -> Option<FileDirectiveType> {
None
}
}
impl WritablePduPacket for FileDataPdu<'_, '_> {
fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, PduError> {
if buf.len() < self.len_written() {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: self.len_written(),
}
.into());
}
let mut current_idx = self.common.write_common_fields_to_bytes(buf)?;
buf[current_idx..current_idx + self.file_data.len()].copy_from_slice(self.file_data);
current_idx += self.file_data.len();
if self.crc_flag() == CrcFlag::WithCrc {
current_idx = add_pdu_crc(buf, current_idx);
}
Ok(current_idx)
}
fn len_written(&self) -> usize {
self.common.pdu_header.header_len() + self.calc_pdu_datafield_len()
}
}
/// File Data PDU creator abstraction.
///
/// This special creator object allows to read into the file data buffer directly. This avoids
/// the need of an additional buffer to create a file data PDU. This structure therefore
/// does not implement the regular [WritablePduPacket] trait.
///
/// For more information, refer to CFDP chapter 5.3.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FileDataPduCreatorWithReservedDatafield<'seg_meta> {
#[cfg_attr(feature = "serde", serde(borrow))]
common: FdPduBase<'seg_meta>,
file_data_len: u64,
}
impl<'seg_meta> FileDataPduCreatorWithReservedDatafield<'seg_meta> {
pub fn new_with_seg_metadata(
pdu_header: PduHeader,
segment_metadata: SegmentMetadata<'seg_meta>,
offset: u64,
file_data_len: u64,
) -> Self {
Self::new_generic(pdu_header, Some(segment_metadata), offset, file_data_len)
}
pub fn new_no_seg_metadata(pdu_header: PduHeader, offset: u64, file_data_len: u64) -> Self {
Self::new_generic(pdu_header, None, offset, file_data_len)
}
pub fn new_generic(
mut pdu_header: PduHeader,
segment_metadata: Option<SegmentMetadata<'seg_meta>>,
offset: u64,
file_data_len: u64,
) -> Self {
pdu_header.pdu_type = PduType::FileData;
if segment_metadata.is_some() {
pdu_header.seg_metadata_flag = SegmentMetadataFlag::Present;
}
let mut pdu = Self {
common: FdPduBase {
pdu_header,
segment_metadata,
offset,
},
file_data_len,
};
pdu.common.pdu_header.pdu_datafield_len = pdu.calc_pdu_datafield_len() as u16;
pdu
}
fn calc_pdu_datafield_len(&self) -> usize {
self.common.calc_pdu_datafield_len(self.file_data_len)
}
pub fn len_written(&self) -> usize {
self.common.pdu_header.header_len() + self.calc_pdu_datafield_len()
}
/// This function performs a partial write by writing all data except the file data
/// and the CRC.
///
/// It returns a [FileDataPduCreatorWithUnwrittenData] which provides a mutable slice to
/// the reserved file data field. The user can read file data into this field directly and
/// then finish the PDU creation using the [FileDataPduCreatorWithUnwrittenData::finish] call.
pub fn write_to_bytes_partially<'buf>(
&self,
buf: &'buf mut [u8],
) -> Result<FileDataPduCreatorWithUnwrittenData<'buf>, PduError> {
if buf.len() < self.len_written() {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: self.len_written(),
}
.into());
}
let mut current_idx = self.common.write_common_fields_to_bytes(buf)?;
let file_data_offset = current_idx as u64;
current_idx += self.file_data_len as usize;
if self.crc_flag() == CrcFlag::WithCrc {
current_idx += 2;
}
Ok(FileDataPduCreatorWithUnwrittenData {
write_buf: &mut buf[0..current_idx],
file_data_offset,
file_data_len: self.file_data_len,
needs_crc: self.crc_flag() == CrcFlag::WithCrc,
})
}
}
impl CfdpPdu for FileDataPduCreatorWithReservedDatafield<'_> {
fn pdu_header(&self) -> &PduHeader {
&self.common.pdu_header
}
fn file_directive_type(&self) -> Option<FileDirectiveType> {
None
}
}
/// This structure is created with [FileDataPduCreatorWithReservedDatafield::write_to_bytes_partially]
/// and provides an API to read file data from the virtual filesystem into the file data PDU buffer
/// directly.
///
/// This structure provides a mutable slice to the reserved file data field. The user can read
/// file data into this field directly and then finish the PDU creation using the
/// [FileDataPduCreatorWithUnwrittenData::finish] call.
pub struct FileDataPduCreatorWithUnwrittenData<'buf> {
write_buf: &'buf mut [u8],
file_data_offset: u64,
file_data_len: u64,
needs_crc: bool,
}
impl FileDataPduCreatorWithUnwrittenData<'_> {
pub fn file_data_field_mut(&mut self) -> &mut [u8] {
&mut self.write_buf[self.file_data_offset as usize
..self.file_data_offset as usize + self.file_data_len as usize]
}
/// This functio needs to be called to add a CRC to the file data PDU where applicable.
///
/// It returns the full written size of the PDU.
pub fn finish(self) -> usize {
if self.needs_crc {
add_pdu_crc(
self.write_buf,
self.file_data_offset as usize + self.file_data_len as usize,
);
}
self.write_buf.len()
}
}
/// This function can be used to calculate the maximum allowed file segment size for
/// a given maximum packet length and the segment metadata if there is any.
pub fn calculate_max_file_seg_len_for_max_packet_len_and_pdu_header(
pdu_header: &PduHeader,
max_packet_len: usize,
segment_metadata: Option<&SegmentMetadata>,
) -> usize {
let mut subtract = pdu_header.header_len();
if segment_metadata.is_some() {
subtract += 1 + segment_metadata.as_ref().unwrap().metadata().unwrap().len();
}
if pdu_header.common_pdu_conf().file_flag == LargeFileFlag::Large {
subtract += 8;
} else {
subtract += 4;
}
if pdu_header.common_pdu_conf().crc_flag == CrcFlag::WithCrc {
subtract += 2;
}
max_packet_len.saturating_sub(subtract)
}
#[cfg(test)]
mod tests {
use crate::cfdp::pdu::file_data::{FileDataPdu, RecordContinuationState, SegmentMetadata};
use super::*;
use crate::cfdp::pdu::tests::{TEST_DEST_ID, TEST_SEQ_NUM, TEST_SRC_ID};
use crate::cfdp::pdu::{CommonPduConfig, PduHeader};
use crate::cfdp::{SegmentMetadataFlag, SegmentationControl};
use crate::util::UbfU8;
use crate::cfdp::{Direction, SegmentMetadataFlag, SegmentationControl, TransmissionMode};
#[cfg(feature = "serde")]
use postcard::{from_bytes, to_allocvec};
#[test]
fn test_basic() {
let src_id = UbfU8::new(1);
let dest_id = UbfU8::new(2);
let transaction_seq_num = UbfU8::new(3);
let common_conf =
CommonPduConfig::new_with_byte_fields(src_id, dest_id, transaction_seq_num).unwrap();
CommonPduConfig::new_with_byte_fields(TEST_SRC_ID, TEST_DEST_ID, TEST_SEQ_NUM).unwrap();
let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
let file_data: [u8; 4] = [1, 2, 3, 4];
let fd_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, 10, &file_data);
@ -243,18 +453,25 @@ mod tests {
assert_eq!(fd_pdu.offset(), 10);
assert!(fd_pdu.segment_metadata().is_none());
assert_eq!(
fd_pdu.written_len(),
fd_pdu.pdu_header.header_len() + core::mem::size_of::<u32>() + 4
fd_pdu.len_written(),
fd_pdu.pdu_header().header_len() + core::mem::size_of::<u32>() + 4
);
assert_eq!(fd_pdu.crc_flag(), CrcFlag::NoCrc);
assert_eq!(fd_pdu.file_flag(), LargeFileFlag::Normal);
assert_eq!(fd_pdu.pdu_type(), PduType::FileData);
assert_eq!(fd_pdu.file_directive_type(), None);
assert_eq!(fd_pdu.transmission_mode(), TransmissionMode::Acknowledged);
assert_eq!(fd_pdu.direction(), Direction::TowardsReceiver);
assert_eq!(fd_pdu.source_id(), TEST_SRC_ID.into());
assert_eq!(fd_pdu.dest_id(), TEST_DEST_ID.into());
assert_eq!(fd_pdu.transaction_seq_num(), TEST_SEQ_NUM.into());
}
#[test]
fn test_serialization() {
let src_id = UbfU8::new(1);
let dest_id = UbfU8::new(2);
let transaction_seq_num = UbfU8::new(3);
let common_conf =
CommonPduConfig::new_with_byte_fields(src_id, dest_id, transaction_seq_num).unwrap();
CommonPduConfig::new_with_byte_fields(TEST_SRC_ID, TEST_DEST_ID, TEST_SEQ_NUM).unwrap();
let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
let file_data: [u8; 4] = [1, 2, 3, 4];
let fd_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, 10, &file_data);
@ -264,11 +481,11 @@ mod tests {
let written = res.unwrap();
assert_eq!(
written,
fd_pdu.pdu_header.header_len() + core::mem::size_of::<u32>() + 4
fd_pdu.pdu_header().header_len() + core::mem::size_of::<u32>() + 4
);
let mut current_idx = fd_pdu.pdu_header.header_len();
let mut current_idx = fd_pdu.pdu_header().header_len();
let file_size = u32::from_be_bytes(
buf[fd_pdu.pdu_header.header_len()..fd_pdu.pdu_header.header_len() + 4]
buf[fd_pdu.pdu_header().header_len()..fd_pdu.pdu_header().header_len() + 4]
.try_into()
.unwrap(),
);
@ -284,12 +501,22 @@ mod tests {
}
#[test]
fn test_deserialization() {
let src_id = UbfU8::new(1);
let dest_id = UbfU8::new(2);
let transaction_seq_num = UbfU8::new(3);
fn test_write_to_vec() {
let common_conf =
CommonPduConfig::new_with_byte_fields(src_id, dest_id, transaction_seq_num).unwrap();
CommonPduConfig::new_with_byte_fields(TEST_SRC_ID, TEST_DEST_ID, TEST_SEQ_NUM).unwrap();
let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
let file_data: [u8; 4] = [1, 2, 3, 4];
let fd_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, 10, &file_data);
let mut buf: [u8; 64] = [0; 64];
let written = fd_pdu.write_to_bytes(&mut buf).unwrap();
let pdu_vec = fd_pdu.to_vec().unwrap();
assert_eq!(buf[0..written], pdu_vec);
}
#[test]
fn test_deserialization() {
let common_conf =
CommonPduConfig::new_with_byte_fields(TEST_SRC_ID, TEST_DEST_ID, TEST_SEQ_NUM).unwrap();
let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
let file_data: [u8; 4] = [1, 2, 3, 4];
let fd_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, 10, &file_data);
@ -301,13 +528,33 @@ mod tests {
assert_eq!(fd_pdu_read_back, fd_pdu);
}
#[test]
fn test_with_crc() {
let mut common_conf =
CommonPduConfig::new_with_byte_fields(TEST_SRC_ID, TEST_DEST_ID, TEST_SEQ_NUM).unwrap();
common_conf.crc_flag = true.into();
let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
let file_data: [u8; 4] = [1, 2, 3, 4];
let fd_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, 10, &file_data);
let mut buf: [u8; 64] = [0; 64];
let written = fd_pdu.write_to_bytes(&mut buf).unwrap();
assert_eq!(written, fd_pdu.len_written());
let finished_pdu_from_raw = FileDataPdu::from_bytes(&buf).unwrap();
assert_eq!(finished_pdu_from_raw, fd_pdu);
buf[written - 1] -= 1;
let crc: u16 = ((buf[written - 2] as u16) << 8) | buf[written - 1] as u16;
let error = FileDataPdu::from_bytes(&buf).unwrap_err();
if let PduError::Checksum(e) = error {
assert_eq!(e, crc);
} else {
panic!("expected crc error");
}
}
#[test]
fn test_with_seg_metadata_serialization() {
let src_id = UbfU8::new(1);
let dest_id = UbfU8::new(2);
let transaction_seq_num = UbfU8::new(3);
let common_conf =
CommonPduConfig::new_with_byte_fields(src_id, dest_id, transaction_seq_num).unwrap();
CommonPduConfig::new_with_byte_fields(TEST_SRC_ID, TEST_DEST_ID, TEST_SEQ_NUM).unwrap();
let pdu_header = PduHeader::new_for_file_data(
common_conf,
0,
@ -323,8 +570,8 @@ mod tests {
assert!(fd_pdu.segment_metadata().is_some());
assert_eq!(*fd_pdu.segment_metadata().unwrap(), segment_meta);
assert_eq!(
fd_pdu.written_len(),
fd_pdu.pdu_header.header_len()
fd_pdu.len_written(),
fd_pdu.pdu_header().header_len()
+ 1
+ seg_metadata.len()
+ core::mem::size_of::<u32>()
@ -334,7 +581,7 @@ mod tests {
fd_pdu
.write_to_bytes(&mut buf)
.expect("writing FD PDU failed");
let mut current_idx = fd_pdu.pdu_header.header_len();
let mut current_idx = fd_pdu.pdu_header().header_len();
assert_eq!(
RecordContinuationState::try_from((buf[current_idx] >> 6) & 0b11).unwrap(),
RecordContinuationState::StartAndEnd
@ -363,16 +610,13 @@ mod tests {
current_idx += 1;
assert_eq!(buf[current_idx], 4);
current_idx += 1;
assert_eq!(current_idx, fd_pdu.written_len());
assert_eq!(current_idx, fd_pdu.len_written());
}
#[test]
fn test_with_seg_metadata_deserialization() {
let src_id = UbfU8::new(1);
let dest_id = UbfU8::new(2);
let transaction_seq_num = UbfU8::new(3);
let common_conf =
CommonPduConfig::new_with_byte_fields(src_id, dest_id, transaction_seq_num).unwrap();
CommonPduConfig::new_with_byte_fields(TEST_SRC_ID, TEST_DEST_ID, TEST_SEQ_NUM).unwrap();
let pdu_header = PduHeader::new_for_file_data(
common_conf,
0,
@ -394,4 +638,177 @@ mod tests {
let fd_pdu_read_back = fd_pdu_read_back.unwrap();
assert_eq!(fd_pdu_read_back, fd_pdu);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_serialization() {
let common_conf =
CommonPduConfig::new_with_byte_fields(TEST_SRC_ID, TEST_DEST_ID, TEST_SEQ_NUM).unwrap();
let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
let file_data: [u8; 4] = [1, 2, 3, 4];
let fd_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, 10, &file_data);
let output = to_allocvec(&fd_pdu).unwrap();
let output_converted_back: FileDataPdu = from_bytes(&output).unwrap();
assert_eq!(output_converted_back, fd_pdu);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_serialization_with_seg_metadata() {
let common_conf =
CommonPduConfig::new_with_byte_fields(TEST_SRC_ID, TEST_DEST_ID, TEST_SEQ_NUM).unwrap();
let pdu_header = PduHeader::new_for_file_data(
common_conf,
0,
SegmentMetadataFlag::Present,
SegmentationControl::WithRecordBoundaryPreservation,
);
let file_data: [u8; 4] = [1, 2, 3, 4];
let seg_metadata: [u8; 4] = [4, 3, 2, 1];
let segment_meta =
SegmentMetadata::new(RecordContinuationState::StartAndEnd, Some(&seg_metadata))
.unwrap();
let fd_pdu = FileDataPdu::new_with_seg_metadata(pdu_header, segment_meta, 10, &file_data);
let output = to_allocvec(&fd_pdu).unwrap();
let output_converted_back: FileDataPdu = from_bytes(&output).unwrap();
assert_eq!(output_converted_back, fd_pdu);
}
#[test]
fn test_fd_pdu_creator_with_reserved_field_no_crc() {
let common_conf =
CommonPduConfig::new_with_byte_fields(TEST_SRC_ID, TEST_DEST_ID, TEST_SEQ_NUM).unwrap();
let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
let test_str = "hello world!";
let fd_pdu = FileDataPduCreatorWithReservedDatafield::new_no_seg_metadata(
pdu_header,
10,
test_str.len() as u64,
);
let mut write_buf: [u8; 64] = [0; 64];
let mut pdu_unwritten = fd_pdu
.write_to_bytes_partially(&mut write_buf)
.expect("partial write failed");
pdu_unwritten
.file_data_field_mut()
.copy_from_slice(test_str.as_bytes());
pdu_unwritten.finish();
let pdu_reader = FileDataPdu::from_bytes(&write_buf).expect("reading file data PDU failed");
assert_eq!(
core::str::from_utf8(pdu_reader.file_data()).expect("reading utf8 string failed"),
"hello world!"
);
}
#[test]
fn test_fd_pdu_creator_with_reserved_field_with_crc() {
let mut common_conf =
CommonPduConfig::new_with_byte_fields(TEST_SRC_ID, TEST_DEST_ID, TEST_SEQ_NUM).unwrap();
common_conf.crc_flag = true.into();
let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
let test_str = "hello world!";
let fd_pdu = FileDataPduCreatorWithReservedDatafield::new_no_seg_metadata(
pdu_header,
10,
test_str.len() as u64,
);
let mut write_buf: [u8; 64] = [0; 64];
let mut pdu_unwritten = fd_pdu
.write_to_bytes_partially(&mut write_buf)
.expect("partial write failed");
pdu_unwritten
.file_data_field_mut()
.copy_from_slice(test_str.as_bytes());
pdu_unwritten.finish();
let pdu_reader = FileDataPdu::from_bytes(&write_buf).expect("reading file data PDU failed");
assert_eq!(
core::str::from_utf8(pdu_reader.file_data()).expect("reading utf8 string failed"),
"hello world!"
);
}
#[test]
fn test_fd_pdu_creator_with_reserved_field_with_crc_without_finish_fails() {
let mut common_conf =
CommonPduConfig::new_with_byte_fields(TEST_SRC_ID, TEST_DEST_ID, TEST_SEQ_NUM).unwrap();
common_conf.crc_flag = true.into();
let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
let test_str = "hello world!";
let fd_pdu = FileDataPduCreatorWithReservedDatafield::new_no_seg_metadata(
pdu_header,
10,
test_str.len() as u64,
);
let mut write_buf: [u8; 64] = [0; 64];
let mut pdu_unwritten = fd_pdu
.write_to_bytes_partially(&mut write_buf)
.expect("partial write failed");
pdu_unwritten
.file_data_field_mut()
.copy_from_slice(test_str.as_bytes());
let pdu_reader_error = FileDataPdu::from_bytes(&write_buf);
assert!(pdu_reader_error.is_err());
let error = pdu_reader_error.unwrap_err();
match error {
PduError::Checksum(_) => (),
_ => {
panic!("unexpected PDU error {}", error)
}
}
}
#[test]
fn test_max_file_seg_calculator_0() {
let pdu_header = PduHeader::new_for_file_data_default(CommonPduConfig::default(), 0);
assert_eq!(
calculate_max_file_seg_len_for_max_packet_len_and_pdu_header(&pdu_header, 64, None),
53
);
}
#[test]
fn test_max_file_seg_calculator_1() {
let common_conf = CommonPduConfig {
crc_flag: CrcFlag::WithCrc,
..Default::default()
};
let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
assert_eq!(
calculate_max_file_seg_len_for_max_packet_len_and_pdu_header(&pdu_header, 64, None),
51
);
}
#[test]
fn test_max_file_seg_calculator_2() {
let common_conf = CommonPduConfig {
file_flag: LargeFileFlag::Large,
..Default::default()
};
let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
assert_eq!(
calculate_max_file_seg_len_for_max_packet_len_and_pdu_header(&pdu_header, 64, None),
49
);
}
#[test]
fn test_max_file_seg_calculator_saturating_sub() {
let common_conf = CommonPduConfig {
file_flag: LargeFileFlag::Large,
..Default::default()
};
let pdu_header = PduHeader::new_for_file_data_default(common_conf, 0);
assert_eq!(
calculate_max_file_seg_len_for_max_packet_len_and_pdu_header(&pdu_header, 15, None),
0
);
assert_eq!(
calculate_max_file_seg_len_for_max_packet_len_and_pdu_header(&pdu_header, 14, None),
0
);
}
}

View File

@ -1,15 +1,21 @@
use crate::cfdp::pdu::{
add_pdu_crc, generic_length_checks_pdu_deserialization, FileDirectiveType, PduError, PduHeader,
};
use crate::cfdp::tlv::{EntityIdTlv, Tlv, TlvType, TlvTypeField};
use crate::cfdp::{ConditionCode, CrcFlag, PduType, TlvLvError};
use crate::{ByteConversionError, SizeMissmatch};
use crate::cfdp::tlv::{
EntityIdTlv, FilestoreResponseTlv, GenericTlv, Tlv, TlvType, TlvTypeField, WritableTlv,
};
use crate::cfdp::{ConditionCode, CrcFlag, Direction, PduType};
use crate::ByteConversionError;
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use super::tlv::ReadableTlv;
use super::{CfdpPdu, InvalidTlvTypeFieldError, WritablePduPacket};
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum DeliveryCode {
Complete = 0,
@ -18,6 +24,7 @@ pub enum DeliveryCode {
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum FileStatus {
DiscardDeliberately = 0b00,
@ -30,17 +37,18 @@ pub enum FileStatus {
///
/// For more information, refer to CFDP chapter 5.2.3.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FinishedPdu<'fs_responses> {
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct FinishedPduCreator<'fs_responses> {
pdu_header: PduHeader,
condition_code: ConditionCode,
delivery_code: DeliveryCode,
file_status: FileStatus,
fs_responses: Option<&'fs_responses [u8]>,
fs_responses:
&'fs_responses [FilestoreResponseTlv<'fs_responses, 'fs_responses, 'fs_responses>],
fault_location: Option<EntityIdTlv>,
}
impl<'fs_responses> FinishedPdu<'fs_responses> {
impl<'fs_responses> FinishedPduCreator<'fs_responses> {
/// Default finished PDU: No error (no fault location field) and no filestore responses.
pub fn new_default(
pdu_header: PduHeader,
@ -52,7 +60,7 @@ impl<'fs_responses> FinishedPdu<'fs_responses> {
ConditionCode::NoError,
delivery_code,
file_status,
None,
&[],
None,
)
}
@ -69,7 +77,7 @@ impl<'fs_responses> FinishedPdu<'fs_responses> {
condition_code,
delivery_code,
file_status,
None,
&[],
Some(fault_location),
)
}
@ -79,10 +87,16 @@ impl<'fs_responses> FinishedPdu<'fs_responses> {
condition_code: ConditionCode,
delivery_code: DeliveryCode,
file_status: FileStatus,
fs_responses: Option<&'fs_responses [u8]>,
fs_responses: &'fs_responses [FilestoreResponseTlv<
'fs_responses,
'fs_responses,
'fs_responses,
>],
fault_location: Option<EntityIdTlv>,
) -> Self {
pdu_header.pdu_type = PduType::FileDirective;
// Enforce correct direction bit.
pdu_header.pdu_conf.direction = Direction::TowardsSender;
let mut finished_pdu = Self {
pdu_header,
condition_code,
@ -94,13 +108,6 @@ impl<'fs_responses> FinishedPdu<'fs_responses> {
finished_pdu.pdu_header.pdu_datafield_len = finished_pdu.calc_pdu_datafield_len() as u16;
finished_pdu
}
pub fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
}
pub fn written_len(&self) -> usize {
self.pdu_header.header_len() + self.calc_pdu_datafield_len()
}
pub fn condition_code(&self) -> ConditionCode {
self.condition_code
@ -114,7 +121,8 @@ impl<'fs_responses> FinishedPdu<'fs_responses> {
self.file_status
}
pub fn filestore_responses(&self) -> Option<&'fs_responses [u8]> {
// If there are no filestore responses, an empty slice will be returned.
pub fn filestore_responses(&self) -> &[FilestoreResponseTlv<'_, '_, '_>] {
self.fs_responses
}
@ -123,23 +131,38 @@ impl<'fs_responses> FinishedPdu<'fs_responses> {
}
fn calc_pdu_datafield_len(&self) -> usize {
let mut base_len = 2;
if let Some(fs_responses) = self.fs_responses {
base_len += fs_responses.len();
let mut datafield_len = 2;
for fs_response in self.fs_responses {
datafield_len += fs_response.len_full();
}
if let Some(fault_location) = self.fault_location {
base_len += fault_location.len_full();
datafield_len += fault_location.len_full();
}
base_len
if self.crc_flag() == CrcFlag::WithCrc {
datafield_len += 2;
}
datafield_len
}
}
impl CfdpPdu for FinishedPduCreator<'_> {
fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
}
pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, PduError> {
let expected_len = self.written_len();
fn file_directive_type(&self) -> Option<FileDirectiveType> {
Some(FileDirectiveType::FinishedPdu)
}
}
impl WritablePduPacket for FinishedPduCreator<'_> {
fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, PduError> {
let expected_len = self.len_written();
if buf.len() < expected_len {
return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: expected_len,
})
}
.into());
}
@ -150,33 +173,89 @@ impl<'fs_responses> FinishedPdu<'fs_responses> {
| ((self.delivery_code as u8) << 2)
| self.file_status as u8;
current_idx += 1;
if let Some(fs_responses) = self.fs_responses {
buf[current_idx..current_idx + fs_responses.len()].copy_from_slice(fs_responses);
current_idx += fs_responses.len();
for fs_responses in self.fs_responses {
current_idx += fs_responses.write_to_bytes(&mut buf[current_idx..])?;
}
if let Some(fault_location) = self.fault_location {
current_idx += fault_location.write_to_be_bytes(&mut buf[current_idx..])?;
current_idx += fault_location.write_to_bytes(&mut buf[current_idx..])?;
}
if self.pdu_header.pdu_conf.crc_flag == CrcFlag::WithCrc {
if self.crc_flag() == CrcFlag::WithCrc {
current_idx = add_pdu_crc(buf, current_idx);
}
Ok(current_idx)
}
fn len_written(&self) -> usize {
self.pdu_header.header_len() + self.calc_pdu_datafield_len()
}
}
/// Helper structure to loop through all filestore responses of a read Finished PDU. It should be
/// noted that iterators in Rust are not fallible, but the TLV creation can fail, for example if
/// the raw TLV data is invalid for some reason. In that case, the iterator will yield [None]
/// because there is no way to recover from this.
///
/// The user can accumulate the length of all TLVs yielded by the iterator and compare it against
/// the full length of the options to check whether the iterator was able to parse all TLVs
/// successfully.
pub struct FilestoreResponseIterator<'buf> {
responses_buf: &'buf [u8],
current_idx: usize,
}
impl<'buf> Iterator for FilestoreResponseIterator<'buf> {
type Item = FilestoreResponseTlv<'buf, 'buf, 'buf>;
fn next(&mut self) -> Option<Self::Item> {
if self.current_idx == self.responses_buf.len() {
return None;
}
let tlv = FilestoreResponseTlv::from_bytes(&self.responses_buf[self.current_idx..]);
// There are not really fallible iterators so we can't continue here..
if tlv.is_err() {
return None;
}
let tlv = tlv.unwrap();
self.current_idx += tlv.len_full();
Some(tlv)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct FinishedPduReader<'buf> {
pdu_header: PduHeader,
condition_code: ConditionCode,
delivery_code: DeliveryCode,
file_status: FileStatus,
fs_responses_raw: &'buf [u8],
fault_location: Option<EntityIdTlv>,
}
impl<'buf> FinishedPduReader<'buf> {
/// Generates [Self] from a raw bytestream.
pub fn from_bytes(buf: &'fs_responses [u8]) -> Result<Self, PduError> {
pub fn new(buf: &'buf [u8]) -> Result<Self, PduError> {
Self::from_bytes(buf)
}
/// Generates [Self] from a raw bytestream.
pub fn from_bytes(buf: &'buf [u8]) -> Result<Self, PduError> {
let (pdu_header, mut current_idx) = PduHeader::from_bytes(buf)?;
let full_len_without_crc = pdu_header.verify_length_and_checksum(buf)?;
let min_expected_len = current_idx + 2;
generic_length_checks_pdu_deserialization(buf, min_expected_len, full_len_without_crc)?;
let directive_type = FileDirectiveType::try_from(buf[current_idx]).map_err(|_| {
PduError::InvalidDirectiveType((buf[current_idx], FileDirectiveType::FinishedPdu))
PduError::InvalidDirectiveType {
found: buf[current_idx],
expected: Some(FileDirectiveType::FinishedPdu),
}
})?;
if directive_type != FileDirectiveType::FinishedPdu {
return Err(PduError::WrongDirectiveType((
directive_type,
FileDirectiveType::FinishedPdu,
)));
return Err(PduError::WrongDirectiveType {
found: directive_type,
expected: FileDirectiveType::FinishedPdu,
});
}
current_idx += 1;
let condition_code = ConditionCode::try_from((buf[current_idx] >> 4) & 0b1111)
@ -185,24 +264,51 @@ impl<'fs_responses> FinishedPdu<'fs_responses> {
let delivery_code = DeliveryCode::try_from((buf[current_idx] >> 2) & 0b1).unwrap();
let file_status = FileStatus::try_from(buf[current_idx] & 0b11).unwrap();
current_idx += 1;
let (fs_responses, fault_location) =
let (fs_responses_raw, fault_location) =
Self::parse_tlv_fields(current_idx, full_len_without_crc, buf)?;
Ok(Self {
pdu_header,
condition_code,
delivery_code,
file_status,
fs_responses,
fs_responses_raw,
fault_location,
})
}
pub fn fs_responses_raw(&self) -> &[u8] {
self.fs_responses_raw
}
pub fn fs_responses_iter(&self) -> FilestoreResponseIterator<'_> {
FilestoreResponseIterator {
responses_buf: self.fs_responses_raw,
current_idx: 0,
}
}
pub fn condition_code(&self) -> ConditionCode {
self.condition_code
}
pub fn delivery_code(&self) -> DeliveryCode {
self.delivery_code
}
pub fn file_status(&self) -> FileStatus {
self.file_status
}
pub fn fault_location(&self) -> Option<EntityIdTlv> {
self.fault_location
}
fn parse_tlv_fields(
mut current_idx: usize,
full_len_without_crc: usize,
buf: &'fs_responses [u8],
) -> Result<(Option<&'fs_responses [u8]>, Option<EntityIdTlv>), PduError> {
let mut fs_responses = None;
buf: &[u8],
) -> Result<(&[u8], Option<EntityIdTlv>), PduError> {
let mut fs_responses: &[u8] = &[];
let mut fault_location = None;
let start_of_fs_responses = current_idx;
// There are leftover filestore response(s) and/or a fault location field.
@ -213,12 +319,12 @@ impl<'fs_responses> FinishedPdu<'fs_responses> {
if tlv_type == TlvType::FilestoreResponse {
current_idx += next_tlv.len_full();
if current_idx == full_len_without_crc {
fs_responses = Some(&buf[start_of_fs_responses..current_idx]);
fs_responses = &buf[start_of_fs_responses..current_idx];
}
} else if tlv_type == TlvType::EntityId {
// At least one FS response is included.
if current_idx > full_len_without_crc {
fs_responses = Some(&buf[start_of_fs_responses..current_idx]);
if current_idx > start_of_fs_responses {
fs_responses = &buf[start_of_fs_responses..current_idx];
}
fault_location = Some(EntityIdTlv::from_bytes(&buf[current_idx..])?);
current_idx += fault_location.as_ref().unwrap().len_full();
@ -226,14 +332,26 @@ impl<'fs_responses> FinishedPdu<'fs_responses> {
// last TLV, everything else would break the whole handling of the packet
// TLVs.
if current_idx != full_len_without_crc {
return Err(PduError::FormatError);
return Err(PduError::Format);
}
} else {
return Err(TlvLvError::InvalidTlvTypeField((tlv_type as u8, None)).into());
return Err(PduError::TlvLv(
InvalidTlvTypeFieldError {
found: tlv_type.into(),
expected: Some(TlvType::FilestoreResponse.into()),
}
.into(),
));
}
}
TlvTypeField::Custom(raw) => {
return Err(TlvLvError::InvalidTlvTypeField((raw, None)).into());
return Err(PduError::TlvLv(
InvalidTlvTypeFieldError {
found: raw,
expected: None,
}
.into(),
));
}
}
}
@ -241,21 +359,55 @@ impl<'fs_responses> FinishedPdu<'fs_responses> {
}
}
impl CfdpPdu for FinishedPduReader<'_> {
fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
}
fn file_directive_type(&self) -> Option<FileDirectiveType> {
Some(FileDirectiveType::FinishedPdu)
}
}
impl PartialEq<FinishedPduCreator<'_>> for FinishedPduReader<'_> {
fn eq(&self, other: &FinishedPduCreator<'_>) -> bool {
self.pdu_header == other.pdu_header
&& self.condition_code == other.condition_code
&& self.delivery_code == other.delivery_code
&& self.file_status == other.file_status
&& self.fault_location == other.fault_location
&& self
.fs_responses_iter()
.zip(other.filestore_responses().iter())
.all(|(a, b)| a == *b)
}
}
impl PartialEq<FinishedPduReader<'_>> for FinishedPduCreator<'_> {
fn eq(&self, other: &FinishedPduReader<'_>) -> bool {
other.eq(self)
}
}
#[cfg(test)]
mod tests {
use crate::cfdp::pdu::finished::{DeliveryCode, FileStatus, FinishedPdu};
use crate::cfdp::pdu::tests::{common_pdu_conf, verify_raw_header};
use super::*;
use crate::cfdp::lv::Lv;
use crate::cfdp::pdu::tests::{
common_pdu_conf, verify_raw_header, TEST_DEST_ID, TEST_SEQ_NUM, TEST_SRC_ID,
};
use crate::cfdp::pdu::{FileDirectiveType, PduHeader};
use crate::cfdp::{ConditionCode, CrcFlag, LargeFileFlag};
use crate::cfdp::tlv::FilestoreResponseTlv;
use crate::cfdp::{ConditionCode, CrcFlag, Direction, LargeFileFlag, TransmissionMode};
fn generic_finished_pdu(
crc_flag: CrcFlag,
fss: LargeFileFlag,
delivery_code: DeliveryCode,
file_status: FileStatus,
) -> FinishedPdu<'static> {
) -> FinishedPduCreator<'static> {
let pdu_header = PduHeader::new_no_file_data(common_pdu_conf(crc_flag, fss), 0);
FinishedPdu::new_default(pdu_header, delivery_code, file_status)
FinishedPduCreator::new_default(pdu_header, delivery_code, file_status)
}
#[test]
@ -267,11 +419,31 @@ mod tests {
FileStatus::Retained,
);
assert_eq!(finished_pdu.condition_code(), ConditionCode::NoError);
assert_eq!(
finished_pdu.pdu_header().pdu_conf.direction,
Direction::TowardsSender
);
assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Complete);
assert_eq!(finished_pdu.file_status(), FileStatus::Retained);
assert_eq!(finished_pdu.filestore_responses(), None);
assert_eq!(finished_pdu.filestore_responses(), &[]);
assert_eq!(finished_pdu.fault_location(), None);
assert_eq!(finished_pdu.pdu_header().pdu_datafield_len, 2);
assert_eq!(finished_pdu.crc_flag(), CrcFlag::NoCrc);
assert_eq!(finished_pdu.file_flag(), LargeFileFlag::Normal);
assert_eq!(finished_pdu.pdu_type(), PduType::FileDirective);
assert_eq!(
finished_pdu.file_directive_type(),
Some(FileDirectiveType::FinishedPdu)
);
assert_eq!(
finished_pdu.transmission_mode(),
TransmissionMode::Acknowledged
);
assert_eq!(finished_pdu.direction(), Direction::TowardsSender);
assert_eq!(finished_pdu.source_id(), TEST_SRC_ID.into());
assert_eq!(finished_pdu.dest_id(), TEST_DEST_ID.into());
assert_eq!(finished_pdu.transaction_seq_num(), TEST_SEQ_NUM.into());
}
fn generic_serialization_test_no_error(delivery_code: DeliveryCode, file_status: FileStatus) {
@ -285,8 +457,13 @@ mod tests {
let written = finished_pdu.write_to_bytes(&mut buf);
assert!(written.is_ok());
let written = written.unwrap();
assert_eq!(written, finished_pdu.written_len());
assert_eq!(written, 9);
assert_eq!(written, finished_pdu.len_written());
assert_eq!(written, finished_pdu.pdu_header().header_len() + 2);
assert_eq!(
finished_pdu.pdu_header().pdu_conf.direction,
Direction::TowardsSender
);
verify_raw_header(finished_pdu.pdu_header(), &buf);
let mut current_idx = finished_pdu.pdu_header().header_len();
assert_eq!(buf[current_idx], FileDirectiveType::FinishedPdu as u8);
@ -318,6 +495,20 @@ mod tests {
generic_serialization_test_no_error(DeliveryCode::Incomplete, FileStatus::Unreported);
}
#[test]
fn test_write_to_vec() {
let finished_pdu = generic_finished_pdu(
CrcFlag::NoCrc,
LargeFileFlag::Normal,
DeliveryCode::Complete,
FileStatus::Retained,
);
let mut buf: [u8; 64] = [0; 64];
let written = finished_pdu.write_to_bytes(&mut buf).unwrap();
let pdu_vec = finished_pdu.to_vec().unwrap();
assert_eq!(buf[0..written], pdu_vec);
}
#[test]
fn test_deserialization_simple() {
let finished_pdu = generic_finished_pdu(
@ -328,9 +519,169 @@ mod tests {
);
let mut buf: [u8; 64] = [0; 64];
finished_pdu.write_to_bytes(&mut buf).unwrap();
let read_back = FinishedPdu::from_bytes(&buf);
let read_back = FinishedPduReader::from_bytes(&buf);
assert!(read_back.is_ok());
let read_back = read_back.unwrap();
assert_eq!(finished_pdu, read_back);
// Use all getter functions here explicitely once.
assert_eq!(finished_pdu.pdu_header(), read_back.pdu_header());
assert_eq!(finished_pdu.condition_code(), read_back.condition_code());
assert_eq!(finished_pdu.fault_location(), read_back.fault_location());
assert_eq!(finished_pdu.file_status(), read_back.file_status());
assert_eq!(finished_pdu.delivery_code(), read_back.delivery_code());
}
#[test]
fn test_serialization_buf_too_small() {
let finished_pdu = generic_finished_pdu(
CrcFlag::NoCrc,
LargeFileFlag::Normal,
DeliveryCode::Complete,
FileStatus::Retained,
);
let mut buf: [u8; 8] = [0; 8];
let error = finished_pdu.write_to_bytes(&mut buf);
assert!(error.is_err());
if let PduError::ByteConversion(ByteConversionError::ToSliceTooSmall { found, expected }) =
error.unwrap_err()
{
assert_eq!(found, 8);
assert_eq!(expected, 9);
} else {
panic!("expected to_slice_too_small error");
}
}
#[test]
fn test_with_crc() {
let finished_pdu = generic_finished_pdu(
CrcFlag::WithCrc,
LargeFileFlag::Normal,
DeliveryCode::Complete,
FileStatus::Retained,
);
let mut buf: [u8; 64] = [0; 64];
let written = finished_pdu.write_to_bytes(&mut buf).unwrap();
assert_eq!(written, finished_pdu.len_written());
let finished_pdu_from_raw = FinishedPduReader::new(&buf).unwrap();
assert_eq!(finished_pdu, finished_pdu_from_raw);
buf[written - 1] -= 1;
let crc: u16 = ((buf[written - 2] as u16) << 8) as u16 | buf[written - 1] as u16;
let error = FinishedPduReader::new(&buf).unwrap_err();
if let PduError::Checksum(e) = error {
assert_eq!(e, crc);
} else {
panic!("expected crc error");
}
}
#[test]
fn test_with_fault_location() {
let pdu_header =
PduHeader::new_no_file_data(common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal), 0);
let finished_pdu = FinishedPduCreator::new_with_error(
pdu_header,
ConditionCode::NakLimitReached,
DeliveryCode::Incomplete,
FileStatus::DiscardDeliberately,
EntityIdTlv::new(TEST_DEST_ID.into()),
);
let finished_pdu_vec = finished_pdu.to_vec().unwrap();
assert_eq!(finished_pdu_vec.len(), 12);
assert_eq!(finished_pdu_vec[9], TlvType::EntityId.into());
assert_eq!(finished_pdu_vec[10], 1);
assert_eq!(finished_pdu_vec[11], TEST_DEST_ID.value_typed());
assert_eq!(
finished_pdu.fault_location().unwrap().entity_id(),
&TEST_DEST_ID.into()
);
}
#[test]
fn test_deserialization_with_fault_location() {
let pdu_header =
PduHeader::new_no_file_data(common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal), 0);
let entity_id_tlv = EntityIdTlv::new(TEST_DEST_ID.into());
let finished_pdu = FinishedPduCreator::new_with_error(
pdu_header,
ConditionCode::NakLimitReached,
DeliveryCode::Incomplete,
FileStatus::DiscardDeliberately,
entity_id_tlv,
);
let finished_pdu_vec = finished_pdu.to_vec().unwrap();
let finished_pdu_deserialized = FinishedPduReader::from_bytes(&finished_pdu_vec).unwrap();
assert_eq!(finished_pdu, finished_pdu_deserialized);
}
#[test]
fn test_deserialization_with_fs_responses() {
let entity_id_tlv = EntityIdTlv::new(TEST_DEST_ID.into());
let first_name = "first.txt";
let first_name_lv = Lv::new_from_str(first_name).unwrap();
let fs_response_0 = FilestoreResponseTlv::new_no_filestore_message(
crate::cfdp::tlv::FilestoreActionCode::CreateFile,
0,
first_name_lv,
None,
)
.unwrap();
let fs_response_1 = FilestoreResponseTlv::new_no_filestore_message(
crate::cfdp::tlv::FilestoreActionCode::DeleteFile,
0,
first_name_lv,
None,
)
.unwrap();
let fs_responses = &[fs_response_0, fs_response_1];
let pdu_header =
PduHeader::new_no_file_data(common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal), 0);
let finished_pdu = FinishedPduCreator::new_generic(
pdu_header,
ConditionCode::NakLimitReached,
DeliveryCode::Incomplete,
FileStatus::DiscardDeliberately,
fs_responses,
Some(entity_id_tlv),
);
let finished_pdu_vec = finished_pdu.to_vec().unwrap();
let finished_pdu_deserialized = FinishedPduReader::from_bytes(&finished_pdu_vec).unwrap();
assert_eq!(finished_pdu_deserialized, finished_pdu);
}
#[test]
fn test_deserialization_with_fs_responses_and_fault_location() {
let first_name = "first.txt";
let first_name_lv = Lv::new_from_str(first_name).unwrap();
let fs_response_0 = FilestoreResponseTlv::new_no_filestore_message(
crate::cfdp::tlv::FilestoreActionCode::CreateFile,
0,
first_name_lv,
None,
)
.unwrap();
let fs_response_1 = FilestoreResponseTlv::new_no_filestore_message(
crate::cfdp::tlv::FilestoreActionCode::DeleteFile,
0,
first_name_lv,
None,
)
.unwrap();
let fs_responses = &[fs_response_0, fs_response_1];
let pdu_header =
PduHeader::new_no_file_data(common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal), 0);
let finished_pdu = FinishedPduCreator::new_generic(
pdu_header,
ConditionCode::NakLimitReached,
DeliveryCode::Incomplete,
FileStatus::DiscardDeliberately,
fs_responses,
None,
);
let finished_pdu_vec = finished_pdu.to_vec().unwrap();
let finished_pdu_deserialized = FinishedPduReader::from_bytes(&finished_pdu_vec).unwrap();
assert_eq!(finished_pdu_deserialized, finished_pdu);
}
}

View File

@ -1,18 +1,24 @@
#[cfg(feature = "alloc")]
use super::tlv::TlvOwned;
use crate::cfdp::lv::Lv;
use crate::cfdp::pdu::{
add_pdu_crc, generic_length_checks_pdu_deserialization, read_fss_field, write_fss_field,
FileDirectiveType, PduError, PduHeader,
};
use crate::cfdp::tlv::Tlv;
use crate::cfdp::{ChecksumType, CrcFlag, LargeFileFlag, PduType};
use crate::{ByteConversionError, SizeMissmatch};
use crate::cfdp::tlv::{Tlv, WritableTlv};
use crate::cfdp::{ChecksumType, CrcFlag, Direction, LargeFileFlag, PduType};
use crate::ByteConversionError;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use super::tlv::ReadableTlv;
use super::{CfdpPdu, WritablePduPacket};
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct MetadataGenericParams {
pub closure_requested: bool,
pub checksum_type: ChecksumType,
@ -48,6 +54,174 @@ pub fn build_metadata_opts_from_vec(
build_metadata_opts_from_slice(buf, tlvs.as_slice())
}
#[cfg(feature = "alloc")]
pub fn build_metadata_opts_from_owned_slice(tlvs: &[TlvOwned]) -> Vec<u8> {
let mut sum_vec = Vec::new();
for tlv in tlvs {
sum_vec.extend(tlv.to_vec());
}
sum_vec
}
/// Metadata PDU creator abstraction.
///
/// This abstraction exposes a specialized API for creating metadata PDUs as specified in
/// CFDP chapter 5.2.5.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct MetadataPduCreator<'src_name, 'dest_name, 'opts> {
pdu_header: PduHeader,
metadata_params: MetadataGenericParams,
src_file_name: Lv<'src_name>,
dest_file_name: Lv<'dest_name>,
options: &'opts [u8],
}
impl<'src_name, 'dest_name, 'opts> MetadataPduCreator<'src_name, 'dest_name, 'opts> {
pub fn new_no_opts(
pdu_header: PduHeader,
metadata_params: MetadataGenericParams,
src_file_name: Lv<'src_name>,
dest_file_name: Lv<'dest_name>,
) -> Self {
Self::new(
pdu_header,
metadata_params,
src_file_name,
dest_file_name,
&[],
)
}
pub fn new_with_opts(
pdu_header: PduHeader,
metadata_params: MetadataGenericParams,
src_file_name: Lv<'src_name>,
dest_file_name: Lv<'dest_name>,
options: &'opts [u8],
) -> Self {
Self::new(
pdu_header,
metadata_params,
src_file_name,
dest_file_name,
options,
)
}
pub fn new(
mut pdu_header: PduHeader,
metadata_params: MetadataGenericParams,
src_file_name: Lv<'src_name>,
dest_file_name: Lv<'dest_name>,
options: &'opts [u8],
) -> Self {
pdu_header.pdu_type = PduType::FileDirective;
pdu_header.pdu_conf.direction = Direction::TowardsReceiver;
let mut pdu = Self {
pdu_header,
metadata_params,
src_file_name,
dest_file_name,
options,
};
pdu.pdu_header.pdu_datafield_len = pdu.calc_pdu_datafield_len() as u16;
pdu
}
pub fn metadata_params(&self) -> &MetadataGenericParams {
&self.metadata_params
}
pub fn src_file_name(&self) -> Lv<'src_name> {
self.src_file_name
}
pub fn dest_file_name(&self) -> Lv<'dest_name> {
self.dest_file_name
}
pub fn options(&self) -> &'opts [u8] {
self.options
}
/// Yield an iterator which can be used to loop through all options. Returns [None] if the
/// options field is empty.
pub fn options_iter(&self) -> OptionsIter<'_> {
OptionsIter {
opt_buf: self.options,
current_idx: 0,
}
}
fn calc_pdu_datafield_len(&self) -> usize {
// One directve type octet and one byte of the directive parameter field.
let mut len = 2;
if self.pdu_header.common_pdu_conf().file_flag == LargeFileFlag::Large {
len += 8;
} else {
len += 4;
}
len += self.src_file_name.len_full();
len += self.dest_file_name.len_full();
len += self.options().len();
if self.crc_flag() == CrcFlag::WithCrc {
len += 2;
}
len
}
}
impl CfdpPdu for MetadataPduCreator<'_, '_, '_> {
fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
}
fn file_directive_type(&self) -> Option<FileDirectiveType> {
Some(FileDirectiveType::MetadataPdu)
}
}
impl WritablePduPacket for MetadataPduCreator<'_, '_, '_> {
fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, PduError> {
let expected_len = self.len_written();
if buf.len() < expected_len {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: expected_len,
}
.into());
}
let mut current_idx = self.pdu_header.write_to_bytes(buf)?;
buf[current_idx] = FileDirectiveType::MetadataPdu as u8;
current_idx += 1;
buf[current_idx] = ((self.metadata_params.closure_requested as u8) << 6)
| (self.metadata_params.checksum_type as u8);
current_idx += 1;
current_idx += write_fss_field(
self.pdu_header.common_pdu_conf().file_flag,
self.metadata_params.file_size,
&mut buf[current_idx..],
)?;
current_idx += self
.src_file_name
.write_to_be_bytes(&mut buf[current_idx..])?;
current_idx += self
.dest_file_name
.write_to_be_bytes(&mut buf[current_idx..])?;
buf[current_idx..current_idx + self.options.len()].copy_from_slice(self.options);
current_idx += self.options.len();
if self.crc_flag() == CrcFlag::WithCrc {
current_idx = add_pdu_crc(buf, current_idx);
}
Ok(current_idx)
}
fn len_written(&self) -> usize {
self.pdu_header.header_len() + self.calc_pdu_datafield_len()
}
}
/// Helper structure to loop through all options of a metadata PDU. It should be noted that
/// iterators in Rust are not fallible, but the TLV creation can fail, for example if the raw TLV
/// data is invalid for some reason. In that case, the iterator will yield [None] because there
@ -79,165 +253,29 @@ impl<'opts> Iterator for OptionsIter<'opts> {
}
}
/// Metadata PDU abstraction.
/// Metadata PDU reader abstraction.
///
/// For more information, refer to CFDP chapter 5.2.5.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
/// This abstraction exposes a specialized API for reading a metadata PDU with minimal copying
/// involved.
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MetadataPdu<'src_name, 'dest_name, 'opts> {
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct MetadataPduReader<'buf> {
pdu_header: PduHeader,
metadata_params: MetadataGenericParams,
#[cfg_attr(feature = "serde", serde(borrow))]
src_file_name: Lv<'src_name>,
src_file_name: Lv<'buf>,
#[cfg_attr(feature = "serde", serde(borrow))]
dest_file_name: Lv<'dest_name>,
options: Option<&'opts [u8]>,
dest_file_name: Lv<'buf>,
options: &'buf [u8],
}
impl<'src_name, 'dest_name, 'opts> MetadataPdu<'src_name, 'dest_name, 'opts> {
pub fn new_no_opts(
pdu_header: PduHeader,
metadata_params: MetadataGenericParams,
src_file_name: Lv<'src_name>,
dest_file_name: Lv<'dest_name>,
) -> Self {
Self::new(
pdu_header,
metadata_params,
src_file_name,
dest_file_name,
None,
)
impl<'raw> MetadataPduReader<'raw> {
pub fn new(buf: &'raw [u8]) -> Result<Self, PduError> {
Self::from_bytes(buf)
}
pub fn new_with_opts(
pdu_header: PduHeader,
metadata_params: MetadataGenericParams,
src_file_name: Lv<'src_name>,
dest_file_name: Lv<'dest_name>,
options: &'opts [u8],
) -> Self {
Self::new(
pdu_header,
metadata_params,
src_file_name,
dest_file_name,
Some(options),
)
}
pub fn new(
mut pdu_header: PduHeader,
metadata_params: MetadataGenericParams,
src_file_name: Lv<'src_name>,
dest_file_name: Lv<'dest_name>,
options: Option<&'opts [u8]>,
) -> Self {
pdu_header.pdu_type = PduType::FileDirective;
let mut pdu = Self {
pdu_header,
metadata_params,
src_file_name,
dest_file_name,
options,
};
pdu.pdu_header.pdu_datafield_len = pdu.calc_pdu_datafield_len() as u16;
pdu
}
pub fn metadata_params(&self) -> &MetadataGenericParams {
&self.metadata_params
}
pub fn src_file_name(&self) -> Lv<'src_name> {
self.src_file_name
}
pub fn dest_file_name(&self) -> Lv<'dest_name> {
self.dest_file_name
}
pub fn options(&self) -> Option<&'opts [u8]> {
self.options
}
/// Yield an iterator which can be used to loop through all options. Returns [None] if the
/// options field is empty.
pub fn options_iter(&self) -> Option<OptionsIter<'opts>> {
self.options?;
Some(OptionsIter {
opt_buf: self.options.unwrap(),
current_idx: 0,
})
}
pub fn written_len(&self) -> usize {
self.pdu_header.header_len() + self.calc_pdu_datafield_len()
}
fn calc_pdu_datafield_len(&self) -> usize {
// One directve type octet and one byte of the directive parameter field.
let mut len = 2;
if self.pdu_header.common_pdu_conf().file_flag == LargeFileFlag::Large {
len += 8;
} else {
len += 4;
}
len += self.src_file_name.len_full();
len += self.dest_file_name.len_full();
if let Some(opts) = self.options {
len += opts.len();
}
if self.pdu_header.pdu_conf.crc_flag == CrcFlag::WithCrc {
len += 2;
}
len
}
pub fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
}
pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, PduError> {
let expected_len = self.written_len();
if buf.len() < expected_len {
return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
found: buf.len(),
expected: expected_len,
})
.into());
}
let mut current_idx = self.pdu_header.write_to_bytes(buf)?;
buf[current_idx] = FileDirectiveType::MetadataPdu as u8;
current_idx += 1;
buf[current_idx] = ((self.metadata_params.closure_requested as u8) << 7)
| (self.metadata_params.checksum_type as u8);
current_idx += 1;
current_idx += write_fss_field(
self.pdu_header.common_pdu_conf().file_flag,
self.metadata_params.file_size,
&mut buf[current_idx..],
)?;
current_idx += self
.src_file_name
.write_to_be_bytes(&mut buf[current_idx..])?;
current_idx += self
.dest_file_name
.write_to_be_bytes(&mut buf[current_idx..])?;
if let Some(opts) = self.options {
buf[current_idx..current_idx + opts.len()].copy_from_slice(opts);
current_idx += opts.len();
}
if self.pdu_header.pdu_conf.crc_flag == CrcFlag::WithCrc {
current_idx = add_pdu_crc(buf, current_idx);
}
Ok(current_idx)
}
pub fn from_bytes<'longest: 'src_name + 'dest_name + 'opts>(
buf: &'longest [u8],
) -> Result<Self, PduError> {
pub fn from_bytes(buf: &'raw [u8]) -> Result<Self, PduError> {
let (pdu_header, mut current_idx) = PduHeader::from_bytes(buf)?;
let full_len_without_crc = pdu_header.verify_length_and_checksum(buf)?;
let is_large_file = pdu_header.pdu_conf.file_flag == LargeFileFlag::Large;
@ -248,13 +286,16 @@ impl<'src_name, 'dest_name, 'opts> MetadataPdu<'src_name, 'dest_name, 'opts> {
}
generic_length_checks_pdu_deserialization(buf, min_expected_len, full_len_without_crc)?;
let directive_type = FileDirectiveType::try_from(buf[current_idx]).map_err(|_| {
PduError::InvalidDirectiveType((buf[current_idx], FileDirectiveType::MetadataPdu))
PduError::InvalidDirectiveType {
found: buf[current_idx],
expected: Some(FileDirectiveType::MetadataPdu),
}
})?;
if directive_type != FileDirectiveType::MetadataPdu {
return Err(PduError::WrongDirectiveType((
directive_type,
FileDirectiveType::MetadataPdu,
)));
return Err(PduError::WrongDirectiveType {
found: directive_type,
expected: FileDirectiveType::MetadataPdu,
});
}
current_idx += 1;
let (fss_len, file_size) =
@ -271,56 +312,95 @@ impl<'src_name, 'dest_name, 'opts> MetadataPdu<'src_name, 'dest_name, 'opts> {
let dest_file_name = Lv::from_bytes(&buf[current_idx..])?;
current_idx += dest_file_name.len_full();
// All left-over bytes are options.
let mut options = None;
if current_idx < full_len_without_crc {
options = Some(&buf[current_idx..full_len_without_crc]);
}
Ok(Self {
pdu_header,
metadata_params,
src_file_name,
dest_file_name,
options,
options: &buf[current_idx..full_len_without_crc],
})
}
/// Yield an iterator which can be used to loop through all options. Returns [None] if the
/// options field is empty.
pub fn options_iter(&self) -> Option<OptionsIter<'_>> {
Some(OptionsIter {
opt_buf: self.options,
current_idx: 0,
})
}
pub fn options(&self) -> &'raw [u8] {
self.options
}
pub fn metadata_params(&self) -> &MetadataGenericParams {
&self.metadata_params
}
pub fn src_file_name(&self) -> Lv {
self.src_file_name
}
pub fn dest_file_name(&self) -> Lv {
self.dest_file_name
}
}
impl CfdpPdu for MetadataPduReader<'_> {
fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
}
fn file_directive_type(&self) -> Option<FileDirectiveType> {
Some(FileDirectiveType::MetadataPdu)
}
}
#[cfg(test)]
pub mod tests {
use alloc::string::ToString;
use crate::cfdp::lv::Lv;
use crate::cfdp::pdu::metadata::{
build_metadata_opts_from_slice, build_metadata_opts_from_vec, MetadataGenericParams,
MetadataPdu,
MetadataPduCreator, MetadataPduReader,
};
use crate::cfdp::pdu::tests::{common_pdu_conf, verify_raw_header};
use crate::cfdp::pdu::tests::{
common_pdu_conf, verify_raw_header, TEST_DEST_ID, TEST_SEQ_NUM, TEST_SRC_ID,
};
use crate::cfdp::pdu::{CfdpPdu, PduError, WritablePduPacket};
use crate::cfdp::pdu::{FileDirectiveType, PduHeader};
use crate::cfdp::tlv::{Tlv, TlvType};
use crate::cfdp::tlv::{ReadableTlv, Tlv, TlvOwned, TlvType, WritableTlv};
use crate::cfdp::{
ChecksumType, CrcFlag, LargeFileFlag, PduType, SegmentMetadataFlag, SegmentationControl,
ChecksumType, CrcFlag, Direction, LargeFileFlag, PduType, SegmentMetadataFlag,
SegmentationControl, TransmissionMode,
};
use std::vec;
const SRC_FILENAME: &'static str = "hello-world.txt";
const DEST_FILENAME: &'static str = "hello-world2.txt";
const SRC_FILENAME: &str = "hello-world.txt";
const DEST_FILENAME: &str = "hello-world2.txt";
fn generic_metadata_pdu<'opts>(
fn generic_metadata_pdu(
crc_flag: CrcFlag,
checksum_type: ChecksumType,
closure_requested: bool,
fss: LargeFileFlag,
opts: Option<&'opts [u8]>,
opts: &[u8],
) -> (
Lv<'static>,
Lv<'static>,
MetadataPdu<'static, 'static, 'opts>,
MetadataPduCreator<'static, 'static, '_>,
) {
let pdu_header = PduHeader::new_no_file_data(common_pdu_conf(crc_flag, fss), 0);
let metadata_params = MetadataGenericParams::new(false, ChecksumType::Crc32, 0x1010);
let metadata_params = MetadataGenericParams::new(closure_requested, checksum_type, 0x1010);
let src_filename = Lv::new_from_str(SRC_FILENAME).expect("Generating string LV failed");
let dest_filename =
Lv::new_from_str(DEST_FILENAME).expect("Generating destination LV failed");
(
src_filename,
dest_filename,
MetadataPdu::new(
MetadataPduCreator::new(
pdu_header,
metadata_params,
src_filename,
@ -332,10 +412,15 @@ pub mod tests {
#[test]
fn test_basic() {
let (src_filename, dest_filename, metadata_pdu) =
generic_metadata_pdu(CrcFlag::NoCrc, LargeFileFlag::Normal, None);
let (src_filename, dest_filename, metadata_pdu) = generic_metadata_pdu(
CrcFlag::NoCrc,
ChecksumType::Crc32,
false,
LargeFileFlag::Normal,
&[],
);
assert_eq!(
metadata_pdu.written_len(),
metadata_pdu.len_written(),
metadata_pdu.pdu_header().header_len()
+ 1
+ 1
@ -345,60 +430,169 @@ pub mod tests {
);
assert_eq!(metadata_pdu.src_file_name(), src_filename);
assert_eq!(metadata_pdu.dest_file_name(), dest_filename);
assert_eq!(metadata_pdu.options(), None);
assert!(metadata_pdu.options().is_empty());
assert_eq!(metadata_pdu.crc_flag(), CrcFlag::NoCrc);
assert_eq!(metadata_pdu.file_flag(), LargeFileFlag::Normal);
assert_eq!(metadata_pdu.pdu_type(), PduType::FileDirective);
assert!(!metadata_pdu.metadata_params().closure_requested);
assert_eq!(
metadata_pdu.metadata_params().checksum_type,
ChecksumType::Crc32
);
assert_eq!(
metadata_pdu.file_directive_type(),
Some(FileDirectiveType::MetadataPdu)
);
assert_eq!(
metadata_pdu.transmission_mode(),
TransmissionMode::Acknowledged
);
assert_eq!(metadata_pdu.direction(), Direction::TowardsReceiver);
assert_eq!(metadata_pdu.source_id(), TEST_SRC_ID.into());
assert_eq!(metadata_pdu.dest_id(), TEST_DEST_ID.into());
assert_eq!(metadata_pdu.transaction_seq_num(), TEST_SEQ_NUM.into());
}
#[test]
fn test_serialization() {
let (src_filename, dest_filename, metadata_pdu) =
generic_metadata_pdu(CrcFlag::NoCrc, LargeFileFlag::Normal, None);
let mut buf: [u8; 64] = [0; 64];
let res = metadata_pdu.write_to_bytes(&mut buf);
assert!(res.is_ok());
let written = res.unwrap();
fn check_metadata_raw_fields(
metadata_pdu: &MetadataPduCreator,
buf: &[u8],
written_bytes: usize,
checksum_type: ChecksumType,
closure_requested: bool,
expected_src_filename: &Lv,
expected_dest_filename: &Lv,
) {
verify_raw_header(metadata_pdu.pdu_header(), buf);
assert_eq!(
written,
written_bytes,
metadata_pdu.pdu_header.header_len()
+ 1
+ 1
+ 4
+ src_filename.len_full()
+ dest_filename.len_full()
+ expected_src_filename.len_full()
+ expected_dest_filename.len_full()
);
verify_raw_header(metadata_pdu.pdu_header(), &buf);
assert_eq!(buf[7], FileDirectiveType::MetadataPdu as u8);
assert_eq!(buf[8] >> 6, false as u8);
assert_eq!(buf[8] & 0b1111, ChecksumType::Crc32 as u8);
assert_eq!(buf[8] >> 6, closure_requested as u8);
assert_eq!(buf[8] & 0b1111, checksum_type as u8);
assert_eq!(u32::from_be_bytes(buf[9..13].try_into().unwrap()), 0x1010);
let mut current_idx = 13;
let src_name_from_raw =
Lv::from_bytes(&buf[current_idx..]).expect("Creating source name LV failed");
assert_eq!(src_name_from_raw, src_filename);
assert_eq!(src_name_from_raw, *expected_src_filename);
current_idx += src_name_from_raw.len_full();
let dest_name_from_raw =
Lv::from_bytes(&buf[current_idx..]).expect("Creating dest name LV failed");
assert_eq!(dest_name_from_raw, dest_filename);
assert_eq!(dest_name_from_raw, *expected_dest_filename);
current_idx += dest_name_from_raw.len_full();
// No options, so no additional data here.
assert_eq!(current_idx, written);
assert_eq!(current_idx, written_bytes);
}
#[test]
fn test_serialization_0() {
let checksum_type = ChecksumType::Crc32;
let closure_requested = false;
let (src_filename, dest_filename, metadata_pdu) = generic_metadata_pdu(
CrcFlag::NoCrc,
checksum_type,
closure_requested,
LargeFileFlag::Normal,
&[],
);
let mut buf: [u8; 64] = [0; 64];
let res = metadata_pdu.write_to_bytes(&mut buf);
assert!(res.is_ok());
let written = res.unwrap();
check_metadata_raw_fields(
&metadata_pdu,
&buf,
written,
checksum_type,
closure_requested,
&src_filename,
&dest_filename,
);
}
#[test]
fn test_serialization_1() {
let checksum_type = ChecksumType::Modular;
let closure_requested = true;
let (src_filename, dest_filename, metadata_pdu) = generic_metadata_pdu(
CrcFlag::NoCrc,
checksum_type,
closure_requested,
LargeFileFlag::Normal,
&[],
);
let mut buf: [u8; 64] = [0; 64];
let res = metadata_pdu.write_to_bytes(&mut buf);
assert!(res.is_ok());
let written = res.unwrap();
check_metadata_raw_fields(
&metadata_pdu,
&buf,
written,
checksum_type,
closure_requested,
&src_filename,
&dest_filename,
);
}
#[test]
fn test_write_to_vec() {
let (_, _, metadata_pdu) = generic_metadata_pdu(
CrcFlag::NoCrc,
ChecksumType::Crc32,
false,
LargeFileFlag::Normal,
&[],
);
let mut buf: [u8; 64] = [0; 64];
let pdu_vec = metadata_pdu.to_vec().unwrap();
let written = metadata_pdu.write_to_bytes(&mut buf).unwrap();
assert_eq!(buf[0..written], pdu_vec);
}
fn compare_read_pdu_to_written_pdu(written: &MetadataPduCreator, read: &MetadataPduReader) {
assert_eq!(written.metadata_params(), read.metadata_params());
assert_eq!(written.src_file_name(), read.src_file_name());
assert_eq!(written.dest_file_name(), read.dest_file_name());
let opts = written.options_iter();
for (tlv_written, tlv_read) in opts.zip(read.options_iter().unwrap()) {
assert_eq!(&tlv_written, &tlv_read);
}
}
#[test]
fn test_deserialization() {
let (_, _, metadata_pdu) =
generic_metadata_pdu(CrcFlag::NoCrc, LargeFileFlag::Normal, None);
let (_, _, metadata_pdu) = generic_metadata_pdu(
CrcFlag::NoCrc,
ChecksumType::Crc32,
true,
LargeFileFlag::Normal,
&[],
);
let mut buf: [u8; 64] = [0; 64];
metadata_pdu.write_to_bytes(&mut buf).unwrap();
let pdu_read_back = MetadataPdu::from_bytes(&buf);
let pdu_read_back = MetadataPduReader::from_bytes(&buf);
assert!(pdu_read_back.is_ok());
let pdu_read_back = pdu_read_back.unwrap();
assert_eq!(pdu_read_back, metadata_pdu);
compare_read_pdu_to_written_pdu(&metadata_pdu, &pdu_read_back);
}
#[test]
fn test_with_crc_flag() {
let (src_filename, dest_filename, metadata_pdu) =
generic_metadata_pdu(CrcFlag::WithCrc, LargeFileFlag::Normal, None);
let (src_filename, dest_filename, metadata_pdu) = generic_metadata_pdu(
CrcFlag::WithCrc,
ChecksumType::Crc32,
true,
LargeFileFlag::Normal,
&[],
);
assert_eq!(metadata_pdu.crc_flag(), CrcFlag::WithCrc);
let mut buf: [u8; 64] = [0; 64];
let write_res = metadata_pdu.write_to_bytes(&mut buf);
assert!(write_res.is_ok());
@ -413,14 +607,20 @@ pub mod tests {
+ dest_filename.len_full()
+ 2
);
let pdu_read_back = MetadataPdu::from_bytes(&buf).unwrap();
assert_eq!(pdu_read_back, metadata_pdu);
assert_eq!(written, metadata_pdu.len_written());
let pdu_read_back = MetadataPduReader::new(&buf).unwrap();
compare_read_pdu_to_written_pdu(&metadata_pdu, &pdu_read_back);
}
#[test]
fn test_with_large_file_flag() {
let (src_filename, dest_filename, metadata_pdu) =
generic_metadata_pdu(CrcFlag::NoCrc, LargeFileFlag::Large, None);
let (src_filename, dest_filename, metadata_pdu) = generic_metadata_pdu(
CrcFlag::NoCrc,
ChecksumType::Crc32,
false,
LargeFileFlag::Large,
&[],
);
let mut buf: [u8; 64] = [0; 64];
let write_res = metadata_pdu.write_to_bytes(&mut buf);
assert!(write_res.is_ok());
@ -434,8 +634,8 @@ pub mod tests {
+ src_filename.len_full()
+ dest_filename.len_full()
);
let pdu_read_back = MetadataPdu::from_bytes(&buf).unwrap();
assert_eq!(pdu_read_back, metadata_pdu);
let pdu_read_back = MetadataPduReader::new(&buf).unwrap();
compare_read_pdu_to_written_pdu(&metadata_pdu, &pdu_read_back);
}
#[test]
@ -477,13 +677,14 @@ pub mod tests {
let tlv1 = Tlv::new_empty(TlvType::FlowLabel);
let msg_to_user: [u8; 4] = [1, 2, 3, 4];
let tlv2 = Tlv::new(TlvType::MsgToUser, &msg_to_user).unwrap();
let tlv_vec = vec![tlv1, tlv2];
let mut opts_buf: [u8; 32] = [0; 32];
let opts_len = build_metadata_opts_from_vec(&mut opts_buf, &tlv_vec).unwrap();
let mut tlv_buf: [u8; 64] = [0; 64];
let opts_len = build_metadata_opts_from_slice(&mut tlv_buf, &[tlv1, tlv2]).unwrap();
let (src_filename, dest_filename, metadata_pdu) = generic_metadata_pdu(
CrcFlag::NoCrc,
ChecksumType::Crc32,
false,
LargeFileFlag::Normal,
Some(&opts_buf[..opts_len]),
&tlv_buf[0..opts_len],
);
let mut buf: [u8; 128] = [0; 128];
let write_res = metadata_pdu.write_to_bytes(&mut buf);
@ -499,19 +700,118 @@ pub mod tests {
+ dest_filename.len_full()
+ opts_len
);
let pdu_read_back = MetadataPdu::from_bytes(&buf).unwrap();
assert_eq!(pdu_read_back, metadata_pdu);
let pdu_read_back = MetadataPduReader::from_bytes(&buf).unwrap();
compare_read_pdu_to_written_pdu(&metadata_pdu, &pdu_read_back);
let opts_iter = pdu_read_back.options_iter();
assert!(opts_iter.is_some());
let opts_iter = opts_iter.unwrap();
let mut accumulated_len = 0;
for (idx, opt) in opts_iter.enumerate() {
assert_eq!(tlv_vec[idx], opt);
if idx == 0 {
assert_eq!(tlv1, opt);
} else if idx == 1 {
assert_eq!(tlv2, opt);
}
accumulated_len += opt.len_full();
}
assert_eq!(accumulated_len, pdu_read_back.options().unwrap().len());
assert_eq!(accumulated_len, pdu_read_back.options().len());
}
#[test]
fn test_with_owned_opts() {
let tlv1 = TlvOwned::new_empty(TlvType::FlowLabel);
let msg_to_user: [u8; 4] = [1, 2, 3, 4];
let tlv2 = TlvOwned::new(TlvType::MsgToUser, &msg_to_user);
let mut all_tlvs = tlv1.to_vec();
all_tlvs.extend(tlv2.to_vec());
let (src_filename, dest_filename, metadata_pdu) = generic_metadata_pdu(
CrcFlag::NoCrc,
ChecksumType::Crc32,
false,
LargeFileFlag::Normal,
&all_tlvs,
);
let mut buf: [u8; 128] = [0; 128];
let write_res = metadata_pdu.write_to_bytes(&mut buf);
assert!(write_res.is_ok());
let written = write_res.unwrap();
assert_eq!(
written,
metadata_pdu.pdu_header.header_len()
+ 1
+ 1
+ 4
+ src_filename.len_full()
+ dest_filename.len_full()
+ all_tlvs.len()
);
let pdu_read_back = MetadataPduReader::from_bytes(&buf).unwrap();
compare_read_pdu_to_written_pdu(&metadata_pdu, &pdu_read_back);
let opts_iter = pdu_read_back.options_iter();
assert!(opts_iter.is_some());
let opts_iter = opts_iter.unwrap();
let mut accumulated_len = 0;
for (idx, opt) in opts_iter.enumerate() {
if idx == 0 {
assert_eq!(tlv1, opt);
} else if idx == 1 {
assert_eq!(tlv2, opt);
}
accumulated_len += opt.len_full();
}
assert_eq!(accumulated_len, pdu_read_back.options().len());
}
#[test]
fn test_invalid_directive_code() {
let (_, _, metadata_pdu) = generic_metadata_pdu(
CrcFlag::NoCrc,
ChecksumType::Crc32,
true,
LargeFileFlag::Large,
&[],
);
let mut metadata_vec = metadata_pdu.to_vec().unwrap();
metadata_vec[7] = 0xff;
let metadata_error = MetadataPduReader::from_bytes(&metadata_vec);
assert!(metadata_error.is_err());
let error = metadata_error.unwrap_err();
if let PduError::InvalidDirectiveType { found, expected } = error {
assert_eq!(found, 0xff);
assert_eq!(expected, Some(FileDirectiveType::MetadataPdu));
assert_eq!(
error.to_string(),
"invalid directive type, found 255, expected Some(MetadataPdu)"
);
} else {
panic!("Expected InvalidDirectiveType error, got {:?}", error);
}
}
#[test]
fn test_wrong_directive_code() {
let (_, _, metadata_pdu) = generic_metadata_pdu(
CrcFlag::NoCrc,
ChecksumType::Crc32,
false,
LargeFileFlag::Large,
&[],
);
let mut metadata_vec = metadata_pdu.to_vec().unwrap();
metadata_vec[7] = FileDirectiveType::EofPdu as u8;
let metadata_error = MetadataPduReader::from_bytes(&metadata_vec);
assert!(metadata_error.is_err());
let error = metadata_error.unwrap_err();
if let PduError::WrongDirectiveType { found, expected } = error {
assert_eq!(found, FileDirectiveType::EofPdu);
assert_eq!(expected, FileDirectiveType::MetadataPdu);
assert_eq!(
error.to_string(),
"wrong directive type, found EofPdu, expected MetadataPdu"
);
} else {
panic!("Expected InvalidDirectiveType error, got {:?}", error);
}
}
#[test]
fn test_corrects_pdu_header() {
let pdu_header = PduHeader::new_for_file_data(
@ -524,8 +824,12 @@ pub mod tests {
let src_filename = Lv::new_from_str(SRC_FILENAME).expect("Generating string LV failed");
let dest_filename =
Lv::new_from_str(DEST_FILENAME).expect("Generating destination LV failed");
let metadata_pdu =
MetadataPdu::new_no_opts(pdu_header, metadata_params, src_filename, dest_filename);
let metadata_pdu = MetadataPduCreator::new_no_opts(
pdu_header,
metadata_params,
src_filename,
dest_filename,
);
assert_eq!(metadata_pdu.pdu_header().pdu_type(), PduType::FileDirective);
}
}

View File

@ -1,19 +1,21 @@
//! CFDP Packet Data Unit (PDU) support.
use crate::cfdp::*;
use crate::util::{UnsignedByteField, UnsignedByteFieldU8, UnsignedEnum};
use crate::ByteConversionError;
use crate::CRC_CCITT_FALSE;
use crate::{ByteConversionError, SizeMissmatch};
use core::fmt::{Display, Formatter};
#[cfg(feature = "std")]
use std::error::Error;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
pub mod ack;
pub mod eof;
pub mod file_data;
pub mod finished;
pub mod metadata;
pub mod nak;
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum FileDirectiveType {
EofPdu = 0x04,
@ -25,128 +27,132 @@ pub enum FileDirectiveType {
KeepAlivePdu = 0x0c,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum PduError {
ByteConversionError(ByteConversionError),
/// Found version ID invalid, not equal to [CFDP_VERSION_2].
#[error("byte conversion error: {0}")]
ByteConversion(#[from] ByteConversionError),
/// Found version ID invalid, not equal to [super::CFDP_VERSION_2].
#[error("CFDP version missmatch, found {0}, expected {ver}", ver = super::CFDP_VERSION_2)]
CfdpVersionMissmatch(u8),
/// Invalid length for the entity ID detected. Only the values 1, 2, 4 and 8 are supported.
#[error("invalid PDU entity ID length {0}, only [1, 2, 4, 8] are allowed")]
InvalidEntityLen(u8),
/// Invalid length for the entity ID detected. Only the values 1, 2, 4 and 8 are supported.
#[error("invalid transaction ID length {0}")]
InvalidTransactionSeqNumLen(u8),
/// The first entry will be the source entity ID length, the second one the destination entity
/// ID length.
SourceDestIdLenMissmatch((usize, usize)),
/// The first tuple entry will be the found directive type, the second entry the expected entry
/// type.
WrongDirectiveType((FileDirectiveType, FileDirectiveType)),
/// The directive type field contained a value not in the range of permitted values.
/// The first tuple entry will be the found raw number, the second entry the expected entry
/// type.
InvalidDirectiveType((u8, FileDirectiveType)),
#[error("missmatch of PDU source ID length {src_id_len} and destination ID length {dest_id_len}")]
SourceDestIdLenMissmatch {
src_id_len: usize,
dest_id_len: usize,
},
/// Wrong directive type, for example when parsing the directive field for a file directive
/// PDU.
#[error("wrong directive type, found {found:?}, expected {expected:?}")]
WrongDirectiveType {
found: FileDirectiveType,
expected: FileDirectiveType,
},
/// The directive type field contained a value not in the range of permitted values. This can
/// also happen if an invalid value is passed to the ACK PDU constructor.
#[error("invalid directive type, found {found:?}, expected {expected:?}")]
InvalidDirectiveType {
found: u8,
expected: Option<FileDirectiveType>,
},
#[error("invalid start or end of scope value for NAK PDU")]
InvalidStartOrEndOfScopeValue,
/// Invalid condition code. Contains the raw detected value.
#[error("invalid condition code {0}")]
InvalidConditionCode(u8),
/// Invalid checksum type which is not part of the checksums listed in the
/// [SANA Checksum Types registry](https://sanaregistry.org/r/checksum_identifiers/).
#[error("invalid checksum type {0}")]
InvalidChecksumType(u8),
#[error("file size {0} too large")]
FileSizeTooLarge(u64),
/// If the CRC flag for a PDU is enabled and the checksum check fails. Contains raw 16-bit CRC.
ChecksumError(u16),
#[error("checksum error for checksum {0}")]
Checksum(u16),
/// Generic error for invalid PDU formats.
FormatError,
#[error("generic PDU format error")]
Format,
/// Error handling a TLV field.
TlvLvError(TlvLvError),
#[error("PDU error: {0}")]
TlvLv(#[from] TlvLvError),
}
impl Display for PduError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
PduError::InvalidEntityLen(raw_id) => {
write!(
f,
"Invalid PDU entity ID length {raw_id}, only [1, 2, 4, 8] are allowed"
)
}
PduError::InvalidTransactionSeqNumLen(raw_id) => {
write!(
f,
"invalid PDUtransaction seq num length {raw_id}, only [1, 2, 4, 8] are allowed"
)
}
PduError::CfdpVersionMissmatch(raw) => {
write!(
f,
"cfdp version missmatch, found {raw}, expected {CFDP_VERSION_2}"
)
}
PduError::SourceDestIdLenMissmatch((src_len, dest_len)) => {
write!(
f,
"missmatch of PDU source length {src_len} and destination length {dest_len}"
)
}
PduError::ByteConversionError(e) => {
write!(f, "{}", e)
}
PduError::FileSizeTooLarge(value) => {
write!(f, "file size value {value} exceeds allowed 32 bit width")
}
PduError::WrongDirectiveType((found, expected)) => {
write!(f, "found directive type {found:?}, expected {expected:?}")
}
PduError::InvalidConditionCode(raw_code) => {
write!(f, "found invalid condition code with raw value {raw_code}")
}
PduError::InvalidDirectiveType((found, expected)) => {
write!(
f,
"invalid directive type value {found}, expected {expected:?} ({})",
*expected as u8
)
}
PduError::InvalidChecksumType(checksum_type) => {
write!(f, "invalid checksum type {checksum_type}")
}
PduError::ChecksumError(checksum) => {
write!(f, "checksum error for CRC {checksum:#04x}")
}
PduError::TlvLvError(error) => {
write!(f, "pdu tlv error: {error}")
}
PduError::FormatError => {
write!(f, "generic PDU format error")
}
}
pub trait WritablePduPacket {
fn len_written(&self) -> usize;
fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, PduError>;
#[cfg(feature = "alloc")]
fn to_vec(&self) -> Result<Vec<u8>, PduError> {
// This is the correct way to do this. See
// [this issue](https://github.com/rust-lang/rust-clippy/issues/4483) for caveats of more
// "efficient" implementations.
let mut vec = alloc::vec![0; self.len_written()];
self.write_to_bytes(&mut vec)?;
Ok(vec)
}
}
#[cfg(feature = "std")]
impl Error for PduError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
PduError::ByteConversionError(e) => Some(e),
PduError::TlvLvError(e) => Some(e),
_ => None,
}
}
}
/// Abstraction trait for fields and properties common for all PDUs.
pub trait CfdpPdu {
fn pdu_header(&self) -> &PduHeader;
impl From<ByteConversionError> for PduError {
fn from(value: ByteConversionError) -> Self {
Self::ByteConversionError(value)
#[inline]
fn source_id(&self) -> UnsignedByteField {
self.pdu_header().common_pdu_conf().source_entity_id
}
}
impl From<TlvLvError> for PduError {
fn from(e: TlvLvError) -> Self {
Self::TlvLvError(e)
#[inline]
fn dest_id(&self) -> UnsignedByteField {
self.pdu_header().common_pdu_conf().dest_entity_id
}
#[inline]
fn transaction_seq_num(&self) -> UnsignedByteField {
self.pdu_header().common_pdu_conf().transaction_seq_num
}
#[inline]
fn transmission_mode(&self) -> TransmissionMode {
self.pdu_header().common_pdu_conf().trans_mode
}
#[inline]
fn direction(&self) -> Direction {
self.pdu_header().common_pdu_conf().direction
}
#[inline]
fn crc_flag(&self) -> CrcFlag {
self.pdu_header().common_pdu_conf().crc_flag
}
#[inline]
fn file_flag(&self) -> LargeFileFlag {
self.pdu_header().common_pdu_conf().file_flag
}
#[inline]
fn pdu_type(&self) -> PduType {
self.pdu_header().pdu_type()
}
fn file_directive_type(&self) -> Option<FileDirectiveType>;
}
/// Common configuration fields for a PDU.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
///
/// Please note that this structure has a custom implementation of [PartialEq] which only
/// compares the values for source entity ID, destination entity ID and transaction sequence
/// number. This permits that those fields can have different widths, as long as the value is the
/// same.
#[derive(Debug, Copy, Clone, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct CommonPduConfig {
source_entity_id: UnsignedByteField,
dest_entity_id: UnsignedByteField,
@ -159,6 +165,7 @@ pub struct CommonPduConfig {
// TODO: Builder pattern might be applicable here..
impl CommonPduConfig {
#[inline]
pub fn new(
source_id: impl Into<UnsignedByteField>,
dest_id: impl Into<UnsignedByteField>,
@ -190,6 +197,7 @@ impl CommonPduConfig {
})
}
#[inline]
pub fn new_with_byte_fields(
source_id: impl Into<UnsignedByteField>,
dest_id: impl Into<UnsignedByteField>,
@ -206,10 +214,12 @@ impl CommonPduConfig {
)
}
#[inline]
pub fn source_id(&self) -> UnsignedByteField {
self.source_entity_id
}
#[inline]
fn source_dest_id_check(
source_id: impl Into<UnsignedByteField>,
dest_id: impl Into<UnsignedByteField>,
@ -217,10 +227,10 @@ impl CommonPduConfig {
let source_id = source_id.into();
let dest_id = dest_id.into();
if source_id.size() != dest_id.size() {
return Err(PduError::SourceDestIdLenMissmatch((
source_id.size(),
dest_id.size(),
)));
return Err(PduError::SourceDestIdLenMissmatch {
src_id_len: source_id.size(),
dest_id_len: dest_id.size(),
});
}
if source_id.size() != 1
&& source_id.size() != 2
@ -232,6 +242,7 @@ impl CommonPduConfig {
Ok((source_id, dest_id))
}
#[inline]
pub fn set_source_and_dest_id(
&mut self,
source_id: impl Into<UnsignedByteField>,
@ -243,6 +254,7 @@ impl CommonPduConfig {
Ok(())
}
#[inline]
pub fn dest_id(&self) -> UnsignedByteField {
self.dest_entity_id
}
@ -251,6 +263,7 @@ impl CommonPduConfig {
impl Default for CommonPduConfig {
/// The defaults for the source ID, destination ID and the transaction sequence number is the
/// [UnsignedByteFieldU8] with an intitial value of 0
#[inline]
fn default() -> Self {
// The new function can not fail for these input parameters.
Self::new(
@ -266,6 +279,19 @@ impl Default for CommonPduConfig {
}
}
impl PartialEq for CommonPduConfig {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.source_entity_id.value() == other.source_entity_id.value()
&& self.dest_entity_id.value() == other.dest_entity_id.value()
&& self.transaction_seq_num.value() == other.transaction_seq_num.value()
&& self.trans_mode == other.trans_mode
&& self.file_flag == other.file_flag
&& self.crc_flag == other.crc_flag
&& self.direction == other.direction
}
}
pub const FIXED_HEADER_LEN: usize = 4;
/// Abstraction for the PDU header common to all CFDP PDUs.
@ -273,6 +299,7 @@ pub const FIXED_HEADER_LEN: usize = 4;
/// For detailed information, refer to chapter 5.1 of the CFDP standard.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct PduHeader {
pdu_type: PduType,
pdu_conf: CommonPduConfig,
@ -282,6 +309,7 @@ pub struct PduHeader {
}
impl PduHeader {
#[inline]
pub fn new_for_file_data(
pdu_conf: CommonPduConfig,
pdu_datafield_len: u16,
@ -297,6 +325,7 @@ impl PduHeader {
)
}
#[inline]
pub fn new_for_file_data_default(pdu_conf: CommonPduConfig, pdu_datafield_len: u16) -> Self {
Self::new_generic(
PduType::FileData,
@ -306,6 +335,7 @@ impl PduHeader {
SegmentationControl::NoRecordBoundaryPreservation,
)
}
#[inline]
pub fn new_no_file_data(pdu_conf: CommonPduConfig, pdu_datafield_len: u16) -> Self {
Self::new_generic(
PduType::FileDirective,
@ -316,6 +346,7 @@ impl PduHeader {
)
}
#[inline]
pub fn new_generic(
pdu_type: PduType,
pdu_conf: CommonPduConfig,
@ -333,6 +364,7 @@ impl PduHeader {
}
/// Returns only the length of the PDU header when written to a raw buffer.
#[inline]
pub fn header_len(&self) -> usize {
FIXED_HEADER_LEN
+ self.pdu_conf.source_entity_id.size()
@ -340,8 +372,14 @@ impl PduHeader {
+ self.pdu_conf.dest_entity_id.size()
}
#[inline]
pub fn pdu_datafield_len(&self) -> usize {
self.pdu_datafield_len.into()
}
/// Returns the full length of the PDU when written to a raw buffer, which is the header length
/// plus the PDU datafield length.
#[inline]
pub fn pdu_len(&self) -> usize {
self.header_len() + self.pdu_datafield_len as usize
}
@ -350,20 +388,20 @@ impl PduHeader {
// Internal note: There is currently no way to pass a PDU configuration like this, but
// this check is still kept for defensive programming.
if self.pdu_conf.source_entity_id.size() != self.pdu_conf.dest_entity_id.size() {
return Err(PduError::SourceDestIdLenMissmatch((
self.pdu_conf.source_entity_id.size(),
self.pdu_conf.dest_entity_id.size(),
)));
return Err(PduError::SourceDestIdLenMissmatch {
src_id_len: self.pdu_conf.source_entity_id.size(),
dest_id_len: self.pdu_conf.dest_entity_id.size(),
});
}
if buf.len()
< FIXED_HEADER_LEN
+ self.pdu_conf.source_entity_id.size()
+ self.pdu_conf.transaction_seq_num.size()
{
return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: FIXED_HEADER_LEN,
})
}
.into());
}
let mut current_idx = 0;
@ -404,17 +442,17 @@ impl PduHeader {
/// flag is not set, it will simply return the PDU length.
pub fn verify_length_and_checksum(&self, buf: &[u8]) -> Result<usize, PduError> {
if buf.len() < self.pdu_len() {
return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
return Err(ByteConversionError::FromSliceTooSmall {
found: buf.len(),
expected: self.pdu_len(),
})
}
.into());
}
if self.pdu_conf.crc_flag == CrcFlag::WithCrc {
let mut digest = CRC_CCITT_FALSE.digest();
digest.update(&buf[..self.pdu_len()]);
if digest.finalize() != 0 {
return Err(PduError::ChecksumError(u16::from_be_bytes(
return Err(PduError::Checksum(u16::from_be_bytes(
buf[self.pdu_len() - 2..self.pdu_len()].try_into().unwrap(),
)));
}
@ -433,11 +471,11 @@ impl PduHeader {
/// function.
pub fn from_bytes(buf: &[u8]) -> Result<(Self, usize), PduError> {
if buf.len() < FIXED_HEADER_LEN {
return Err(PduError::ByteConversionError(
ByteConversionError::FromSliceTooSmall(SizeMissmatch {
return Err(PduError::ByteConversion(
ByteConversionError::FromSliceTooSmall {
found: buf.len(),
expected: FIXED_HEADER_LEN,
}),
},
));
}
let cfdp_version_raw = (buf[0] >> 5) & 0b111;
@ -472,10 +510,10 @@ impl PduHeader {
));
}
if buf.len() < (4 + 2 * expected_len_entity_ids + expected_len_seq_num) {
return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
return Err(ByteConversionError::FromSliceTooSmall {
found: buf.len(),
expected: 4 + 2 * expected_len_entity_ids + expected_len_seq_num,
})
}
.into());
}
let mut current_idx = 4;
@ -514,10 +552,13 @@ impl PduHeader {
current_idx,
))
}
#[inline]
pub fn pdu_type(&self) -> PduType {
self.pdu_type
}
#[inline]
pub fn common_pdu_conf(&self) -> &CommonPduConfig {
&self.pdu_conf
}
@ -525,6 +566,8 @@ impl PduHeader {
pub fn seg_metadata_flag(&self) -> SegmentMetadataFlag {
self.seg_metadata_flag
}
#[inline]
pub fn seg_ctrl(&self) -> SegmentationControl {
self.seg_ctrl
}
@ -571,17 +614,17 @@ pub(crate) fn generic_length_checks_pdu_deserialization(
) -> Result<(), ByteConversionError> {
// Buffer too short to hold additional expected minimum datasize.
if buf.len() < min_expected_len {
return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
return Err(ByteConversionError::FromSliceTooSmall {
found: buf.len(),
expected: min_expected_len,
}));
});
}
// This can happen if the PDU datafield length value is invalid.
if full_len_without_crc < min_expected_len {
return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
return Err(ByteConversionError::FromSliceTooSmall {
found: full_len_without_crc,
expected: min_expected_len,
}));
});
}
Ok(())
}
@ -596,23 +639,26 @@ pub(crate) fn add_pdu_crc(buf: &mut [u8], mut current_idx: usize) -> usize {
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use crate::cfdp::pdu::{CommonPduConfig, PduError, PduHeader, FIXED_HEADER_LEN};
use crate::cfdp::{
CrcFlag, Direction, LargeFileFlag, PduType, SegmentMetadataFlag, SegmentationControl,
TransmissionMode, CFDP_VERSION_2,
};
use crate::util::{
UbfU8, UnsignedByteField, UnsignedByteFieldU16, UnsignedByteFieldU8, UnsignedEnum,
UbfU16, UbfU8, UnsignedByteField, UnsignedByteFieldU16, UnsignedByteFieldU8, UnsignedEnum,
};
use crate::ByteConversionError;
use std::format;
pub(crate) const TEST_SRC_ID: UbfU8 = UbfU8::new(5);
pub(crate) const TEST_DEST_ID: UbfU8 = UbfU8::new(10);
pub(crate) const TEST_SEQ_NUM: UbfU8 = UbfU8::new(20);
pub(crate) fn common_pdu_conf(crc_flag: CrcFlag, fss: LargeFileFlag) -> CommonPduConfig {
let src_id = UbfU8::new(5);
let dest_id = UbfU8::new(10);
let transaction_seq_num = UbfU8::new(20);
let mut pdu_conf =
CommonPduConfig::new_with_byte_fields(src_id, dest_id, transaction_seq_num)
CommonPduConfig::new_with_byte_fields(TEST_SRC_ID, TEST_DEST_ID, TEST_SEQ_NUM)
.expect("Generating common PDU config");
pdu_conf.crc_flag = crc_flag;
pdu_conf.file_flag = fss;
@ -623,7 +669,6 @@ mod tests {
assert_eq!((buf[0] >> 5) & 0b111, CFDP_VERSION_2);
// File directive
assert_eq!((buf[0] >> 4) & 1, pdu_conf.pdu_type as u8);
// Towards receiver
assert_eq!((buf[0] >> 3) & 1, pdu_conf.pdu_conf.direction as u8);
// Acknowledged
assert_eq!((buf[0] >> 2) & 1, pdu_conf.pdu_conf.trans_mode as u8);
@ -669,7 +714,7 @@ mod tests {
.try_into()
.unwrap()
),
ubf.value() as u64
ubf.value()
),
_ => panic!("invalid entity ID length"),
}
@ -704,6 +749,17 @@ mod tests {
assert_eq!(pdu_header.header_len(), 7);
}
#[test]
fn test_common_pdu_conf_partial_eq() {
let common_pdu_cfg_0 =
CommonPduConfig::new_with_byte_fields(UbfU8::new(1), UbfU8::new(2), UbfU8::new(3))
.expect("common config creation failed");
let common_pdu_cfg_1 =
CommonPduConfig::new_with_byte_fields(UbfU16::new(1), UbfU16::new(2), UbfU16::new(3))
.expect("common config creation failed");
assert_eq!(common_pdu_cfg_0, common_pdu_cfg_1);
}
#[test]
fn test_basic_state_default() {
let default_conf = CommonPduConfig::default();
@ -718,6 +774,8 @@ mod tests {
assert_eq!(default_conf.crc_flag, CrcFlag::NoCrc);
assert_eq!(default_conf.file_flag, LargeFileFlag::Normal);
}
#[test]
fn test_pdu_header_setter() {
let src_id = UnsignedByteFieldU8::new(1);
let dest_id = UnsignedByteFieldU8::new(2);
@ -747,6 +805,7 @@ mod tests {
// 4 byte fixed header plus three bytes src, dest ID and transaction ID
assert_eq!(res.unwrap(), 7);
verify_raw_header(&pdu_header, &buf);
assert_eq!(pdu_header.pdu_datafield_len(), 5);
}
#[test]
@ -840,6 +899,10 @@ mod tests {
let error = res.unwrap_err();
if let PduError::CfdpVersionMissmatch(raw_version) = error {
assert_eq!(raw_version, CFDP_VERSION_2 + 1);
assert_eq!(
error.to_string(),
"CFDP version missmatch, found 2, expected 1"
);
} else {
panic!("invalid exception: {}", error);
}
@ -851,11 +914,13 @@ mod tests {
let res = PduHeader::from_bytes(&buf);
assert!(res.is_err());
let error = res.unwrap_err();
if let PduError::ByteConversionError(ByteConversionError::FromSliceTooSmall(missmatch)) =
error
if let PduError::ByteConversion(ByteConversionError::FromSliceTooSmall {
found,
expected,
}) = error
{
assert_eq!(missmatch.found, 3);
assert_eq!(missmatch.expected, FIXED_HEADER_LEN);
assert_eq!(found, 3);
assert_eq!(expected, FIXED_HEADER_LEN);
} else {
panic!("invalid exception: {}", error);
}
@ -875,11 +940,17 @@ mod tests {
let header = PduHeader::from_bytes(&buf[0..6]);
assert!(header.is_err());
let error = header.unwrap_err();
if let PduError::ByteConversionError(ByteConversionError::FromSliceTooSmall(missmatch)) =
error
if let PduError::ByteConversion(ByteConversionError::FromSliceTooSmall {
found,
expected,
}) = error
{
assert_eq!(missmatch.found, 6);
assert_eq!(missmatch.expected, 7);
assert_eq!(found, 6);
assert_eq!(expected, 7);
assert_eq!(
error.to_string(),
"byte conversion error: source slice with size 6 too small, expected at least 7 bytes"
);
}
}
@ -907,6 +978,10 @@ mod tests {
let error = pdu_conf_res.unwrap_err();
if let PduError::InvalidEntityLen(len) = error {
assert_eq!(len, 3);
assert_eq!(
error.to_string(),
"invalid PDU entity ID length 3, only [1, 2, 4, 8] are allowed"
);
} else {
panic!("Invalid exception: {}", error)
}
@ -920,9 +995,17 @@ mod tests {
CommonPduConfig::new_with_byte_fields(src_id, dest_id, transaction_seq_id);
assert!(pdu_conf_res.is_err());
let error = pdu_conf_res.unwrap_err();
if let PduError::SourceDestIdLenMissmatch((src_len, dest_len)) = error {
assert_eq!(src_len, 1);
assert_eq!(dest_len, 2);
if let PduError::SourceDestIdLenMissmatch {
src_id_len,
dest_id_len,
} = error
{
assert_eq!(src_id_len, 1);
assert_eq!(dest_id_len, 2);
assert_eq!(
error.to_string(),
"missmatch of PDU source ID length 1 and destination ID length 2"
);
}
}
@ -973,4 +1056,20 @@ mod tests {
panic!("invalid exception {:?}", error)
}
}
#[test]
fn test_pdu_error_clonable_and_comparable() {
let pdu_error = PduError::InvalidEntityLen(0);
let pdu_error_2 = pdu_error;
assert_eq!(pdu_error, pdu_error_2);
}
#[test]
fn test_pdu_config_clonable_and_comparable() {
let common_pdu_cfg_0 =
CommonPduConfig::new_with_byte_fields(UbfU8::new(1), UbfU8::new(2), UbfU8::new(3))
.expect("common config creation failed");
let common_pdu_cfg_1 = common_pdu_cfg_0;
assert_eq!(common_pdu_cfg_0, common_pdu_cfg_1);
}
}

806
src/cfdp/pdu/nak.rs Normal file
View File

@ -0,0 +1,806 @@
use crate::{
cfdp::{CrcFlag, Direction, LargeFileFlag},
ByteConversionError,
};
use core::{marker::PhantomData, mem::size_of};
use super::{
add_pdu_crc, generic_length_checks_pdu_deserialization, CfdpPdu, FileDirectiveType, PduError,
PduHeader, WritablePduPacket,
};
/// Helper type to encapsulate both normal file size segment requests and large file size segment
/// requests.
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum SegmentRequests<'a> {
U32Pairs(&'a [(u32, u32)]),
U64Pairs(&'a [(u64, u64)]),
}
impl SegmentRequests<'_> {
pub fn is_empty(&self) -> bool {
match self {
SegmentRequests::U32Pairs(pairs) => pairs.is_empty(),
SegmentRequests::U64Pairs(pairs) => pairs.is_empty(),
}
}
}
/// NAK PDU abstraction specialized in the creation of NAK PDUs.
///
/// It exposes a specialized API which simplifies to generate these NAK PDUs with the
/// format according to CFDP chapter 5.2.6.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct NakPduCreator<'seg_reqs> {
pdu_header: PduHeader,
start_of_scope: u64,
end_of_scope: u64,
segment_requests: Option<SegmentRequests<'seg_reqs>>,
}
impl<'seg_reqs> NakPduCreator<'seg_reqs> {
/// Please note that the start of scope and the end of scope need to be smaller or equal
/// to [u32::MAX] if the large file flag of the passed PDU configuration is
/// [LargeFileFlag::Normal].
///
/// ## Errrors
///
pub fn new_no_segment_requests(
pdu_header: PduHeader,
start_of_scope: u64,
end_of_scope: u64,
) -> Result<NakPduCreator<'seg_reqs>, PduError> {
Self::new_generic(pdu_header, start_of_scope, end_of_scope, None)
}
/// Default constructor for normal file sizes.
pub fn new(
pdu_header: PduHeader,
start_of_scope: u32,
end_of_scope: u32,
segment_requests: &'seg_reqs [(u32, u32)],
) -> Result<NakPduCreator, PduError> {
let mut passed_segment_requests = None;
if !segment_requests.is_empty() {
passed_segment_requests = Some(SegmentRequests::U32Pairs(segment_requests));
}
Self::new_generic(
pdu_header,
start_of_scope.into(),
end_of_scope.into(),
passed_segment_requests,
)
}
pub fn new_large_file_size(
pdu_header: PduHeader,
start_of_scope: u64,
end_of_scope: u64,
segment_requests: &'seg_reqs [(u64, u64)],
) -> Result<NakPduCreator, PduError> {
let mut passed_segment_requests = None;
if !segment_requests.is_empty() {
passed_segment_requests = Some(SegmentRequests::U64Pairs(segment_requests));
}
Self::new_generic(
pdu_header,
start_of_scope,
end_of_scope,
passed_segment_requests,
)
}
fn new_generic(
mut pdu_header: PduHeader,
start_of_scope: u64,
end_of_scope: u64,
segment_requests: Option<SegmentRequests<'seg_reqs>>,
) -> Result<NakPduCreator, PduError> {
// Force correct direction flag.
pdu_header.pdu_conf.direction = Direction::TowardsSender;
if let Some(ref segment_requests) = segment_requests {
match segment_requests {
SegmentRequests::U32Pairs(_) => {
if start_of_scope > u32::MAX as u64 || end_of_scope > u32::MAX as u64 {
return Err(PduError::InvalidStartOrEndOfScopeValue);
}
pdu_header.pdu_conf.file_flag = LargeFileFlag::Normal;
}
SegmentRequests::U64Pairs(_) => {
pdu_header.pdu_conf.file_flag = LargeFileFlag::Large;
}
}
};
let mut nak_pdu = Self {
pdu_header,
start_of_scope,
end_of_scope,
segment_requests,
};
nak_pdu.pdu_header.pdu_datafield_len = nak_pdu.calc_pdu_datafield_len() as u16;
Ok(nak_pdu)
}
pub fn start_of_scope(&self) -> u64 {
self.start_of_scope
}
pub fn end_of_scope(&self) -> u64 {
self.end_of_scope
}
pub fn segment_requests(&self) -> Option<&SegmentRequests> {
self.segment_requests.as_ref()
}
pub fn num_segment_reqs(&self) -> usize {
match &self.segment_requests {
Some(seg_reqs) => match seg_reqs {
SegmentRequests::U32Pairs(pairs) => pairs.len(),
SegmentRequests::U64Pairs(pairs) => pairs.len(),
},
None => 0,
}
}
pub fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
}
fn calc_pdu_datafield_len(&self) -> usize {
let mut datafield_len = 1;
if self.file_flag() == LargeFileFlag::Normal {
datafield_len += 8;
datafield_len += self.num_segment_reqs() * 8;
} else {
datafield_len += 16;
datafield_len += self.num_segment_reqs() * 16;
}
if self.crc_flag() == CrcFlag::WithCrc {
datafield_len += 2;
}
datafield_len
}
}
impl CfdpPdu for NakPduCreator<'_> {
fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
}
fn file_directive_type(&self) -> Option<FileDirectiveType> {
Some(FileDirectiveType::NakPdu)
}
}
impl WritablePduPacket for NakPduCreator<'_> {
fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, PduError> {
let expected_len = self.len_written();
if buf.len() < expected_len {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: expected_len,
}
.into());
}
let mut current_idx = self.pdu_header.write_to_bytes(buf)?;
buf[current_idx] = FileDirectiveType::NakPdu as u8;
current_idx += 1;
let mut write_start_end_of_scope_normal = || {
let start_of_scope = u32::try_from(self.start_of_scope).unwrap();
let end_of_scope = u32::try_from(self.end_of_scope).unwrap();
buf[current_idx..current_idx + 4].copy_from_slice(&start_of_scope.to_be_bytes());
current_idx += 4;
buf[current_idx..current_idx + 4].copy_from_slice(&end_of_scope.to_be_bytes());
current_idx += 4;
};
if let Some(ref seg_reqs) = self.segment_requests {
match seg_reqs {
SegmentRequests::U32Pairs(pairs) => {
// Unwrap is okay here, the API should prevent invalid values which would trigger a
// panic here.
write_start_end_of_scope_normal();
for (next_start_offset, next_end_offset) in *pairs {
buf[current_idx..current_idx + 4]
.copy_from_slice(&next_start_offset.to_be_bytes());
current_idx += 4;
buf[current_idx..current_idx + 4]
.copy_from_slice(&next_end_offset.to_be_bytes());
current_idx += 4;
}
}
SegmentRequests::U64Pairs(pairs) => {
buf[current_idx..current_idx + 8]
.copy_from_slice(&self.start_of_scope.to_be_bytes());
current_idx += 8;
buf[current_idx..current_idx + 8]
.copy_from_slice(&self.end_of_scope.to_be_bytes());
current_idx += 8;
for (next_start_offset, next_end_offset) in *pairs {
buf[current_idx..current_idx + 8]
.copy_from_slice(&next_start_offset.to_be_bytes());
current_idx += 8;
buf[current_idx..current_idx + 8]
.copy_from_slice(&next_end_offset.to_be_bytes());
current_idx += 8;
}
}
}
} else {
write_start_end_of_scope_normal();
}
if self.crc_flag() == CrcFlag::WithCrc {
current_idx = add_pdu_crc(buf, current_idx);
}
Ok(current_idx)
}
fn len_written(&self) -> usize {
self.pdu_header.header_len() + self.calc_pdu_datafield_len()
}
}
/// Special iterator type for the NAK PDU which allows to iterate over both normal and large file
/// segment requests.
#[derive(Debug)]
pub struct SegmentRequestIter<'a, T> {
seq_req_raw: &'a [u8],
current_idx: usize,
phantom: core::marker::PhantomData<T>,
}
pub trait SegReqFromBytes {
fn from_bytes(bytes: &[u8]) -> Self;
}
impl SegReqFromBytes for u32 {
fn from_bytes(bytes: &[u8]) -> u32 {
u32::from_be_bytes(bytes.try_into().unwrap())
}
}
impl SegReqFromBytes for u64 {
fn from_bytes(bytes: &[u8]) -> u64 {
u64::from_be_bytes(bytes.try_into().unwrap())
}
}
impl<'a, T> Iterator for SegmentRequestIter<'a, T>
where
T: SegReqFromBytes,
{
type Item = (T, T);
fn next(&mut self) -> Option<Self::Item> {
let value = self.next_at_offset(self.current_idx);
self.current_idx += 2 * size_of::<T>();
value
}
}
impl<'a, 'b> PartialEq<SegmentRequests<'a>> for SegmentRequestIter<'b, u32> {
fn eq(&self, other: &SegmentRequests) -> bool {
match other {
SegmentRequests::U32Pairs(pairs) => self.compare_pairs(pairs),
SegmentRequests::U64Pairs(pairs) => {
if pairs.is_empty() && self.seq_req_raw.is_empty() {
return true;
}
false
}
}
}
}
impl<'a, 'b> PartialEq<SegmentRequests<'a>> for SegmentRequestIter<'b, u64> {
fn eq(&self, other: &SegmentRequests) -> bool {
match other {
SegmentRequests::U32Pairs(pairs) => {
if pairs.is_empty() && self.seq_req_raw.is_empty() {
return true;
}
false
}
SegmentRequests::U64Pairs(pairs) => self.compare_pairs(pairs),
}
}
}
impl<'a, T> SegmentRequestIter<'a, T>
where
T: SegReqFromBytes + PartialEq,
{
fn compare_pairs(&self, pairs: &[(T, T)]) -> bool {
if pairs.is_empty() && self.seq_req_raw.is_empty() {
return true;
}
let size = size_of::<T>();
if pairs.len() * 2 * size != self.seq_req_raw.len() {
return false;
}
for (i, pair) in pairs.iter().enumerate() {
let next_val = self.next_at_offset(i * 2 * size).unwrap();
if next_val != *pair {
return false;
}
}
true
}
}
impl<T: SegReqFromBytes> SegmentRequestIter<'_, T> {
fn next_at_offset(&self, mut offset: usize) -> Option<(T, T)> {
if offset + size_of::<T>() * 2 > self.seq_req_raw.len() {
return None;
}
let start_offset = T::from_bytes(&self.seq_req_raw[offset..offset + size_of::<T>()]);
offset += size_of::<T>();
let end_offset = T::from_bytes(&self.seq_req_raw[offset..offset + size_of::<T>()]);
Some((start_offset, end_offset))
}
}
/// NAK PDU abstraction specialized in the reading NAK PDUs from a raw bytestream.
///
/// This is a zero-copy class where the segment requests can be read using a special iterator
/// API without the need to copy them.
///
/// The NAK format is expected to be conforming to CFDP chapter 5.2.6.
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct NakPduReader<'seg_reqs> {
pdu_header: PduHeader,
start_of_scope: u64,
end_of_scope: u64,
seg_reqs_raw: &'seg_reqs [u8],
}
impl CfdpPdu for NakPduReader<'_> {
fn pdu_header(&self) -> &PduHeader {
&self.pdu_header
}
fn file_directive_type(&self) -> Option<FileDirectiveType> {
Some(FileDirectiveType::NakPdu)
}
}
impl<'seg_reqs> NakPduReader<'seg_reqs> {
pub fn new(buf: &'seg_reqs [u8]) -> Result<NakPduReader, PduError> {
Self::from_bytes(buf)
}
pub fn from_bytes(buf: &'seg_reqs [u8]) -> Result<NakPduReader, PduError> {
let (pdu_header, mut current_idx) = PduHeader::from_bytes(buf)?;
let full_len_without_crc = pdu_header.verify_length_and_checksum(buf)?;
// Minimum length of 9: 1 byte directive field and start and end of scope for normal file
// size.
generic_length_checks_pdu_deserialization(buf, 9, full_len_without_crc)?;
let directive_type = FileDirectiveType::try_from(buf[current_idx]).map_err(|_| {
PduError::InvalidDirectiveType {
found: buf[current_idx],
expected: Some(FileDirectiveType::NakPdu),
}
})?;
if directive_type != FileDirectiveType::NakPdu {
return Err(PduError::WrongDirectiveType {
found: directive_type,
expected: FileDirectiveType::AckPdu,
});
}
current_idx += 1;
let start_of_scope;
let end_of_scope;
if pdu_header.common_pdu_conf().file_flag == LargeFileFlag::Large {
if current_idx + 16 > buf.len() {
return Err(PduError::ByteConversion(
ByteConversionError::FromSliceTooSmall {
found: buf.len(),
expected: current_idx + 16,
},
));
}
start_of_scope =
u64::from_be_bytes(buf[current_idx..current_idx + 8].try_into().unwrap());
current_idx += 8;
end_of_scope =
u64::from_be_bytes(buf[current_idx..current_idx + 8].try_into().unwrap());
current_idx += 8;
} else {
start_of_scope =
u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap()) as u64;
current_idx += 4;
end_of_scope =
u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap()) as u64;
current_idx += 4;
}
Ok(Self {
pdu_header,
start_of_scope,
end_of_scope,
seg_reqs_raw: &buf[current_idx..full_len_without_crc],
})
}
pub fn start_of_scope(&self) -> u64 {
self.start_of_scope
}
pub fn end_of_scope(&self) -> u64 {
self.end_of_scope
}
pub fn num_segment_reqs(&self) -> usize {
if self.seg_reqs_raw.is_empty() {
return 0;
}
if self.file_flag() == LargeFileFlag::Normal {
self.seg_reqs_raw.len() / 8
} else {
self.seg_reqs_raw.len() / 16
}
}
/// This function returns [None] if this NAK PDUs contains segment requests for a large file.
pub fn get_normal_segment_requests_iterator(&self) -> Option<SegmentRequestIter<'_, u32>> {
if self.file_flag() == LargeFileFlag::Large {
return None;
}
Some(SegmentRequestIter {
seq_req_raw: self.seg_reqs_raw,
current_idx: 0,
phantom: PhantomData,
})
}
/// This function returns [None] if this NAK PDUs contains segment requests for a normal file.
pub fn get_large_segment_requests_iterator(&self) -> Option<SegmentRequestIter<'_, u64>> {
if self.file_flag() == LargeFileFlag::Normal {
return None;
}
Some(SegmentRequestIter {
seq_req_raw: self.seg_reqs_raw,
current_idx: 0,
phantom: PhantomData,
})
}
}
impl<'a, 'b> PartialEq<NakPduCreator<'a>> for NakPduReader<'b> {
fn eq(&self, other: &NakPduCreator<'a>) -> bool {
if self.pdu_header() != other.pdu_header()
|| self.end_of_scope() != other.end_of_scope()
|| self.start_of_scope() != other.start_of_scope()
{
return false;
}
// Check if both segment requests are empty or None
match (self.seg_reqs_raw.is_empty(), other.segment_requests()) {
(true, None) => true,
(true, Some(seg_reqs)) => seg_reqs.is_empty(),
(false, None) => false,
_ => {
// Compare based on file_flag
if self.file_flag() == LargeFileFlag::Normal {
let normal_iter = self.get_normal_segment_requests_iterator().unwrap();
normal_iter == *other.segment_requests().unwrap()
} else {
let large_iter = self.get_large_segment_requests_iterator().unwrap();
large_iter == *other.segment_requests().unwrap()
}
}
}
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use crate::cfdp::{
pdu::tests::{common_pdu_conf, verify_raw_header, TEST_DEST_ID, TEST_SEQ_NUM, TEST_SRC_ID},
PduType, TransmissionMode,
};
use super::*;
fn check_generic_fields(nak_pdu: &impl CfdpPdu) {
assert_eq!(nak_pdu.crc_flag(), CrcFlag::NoCrc);
assert_eq!(nak_pdu.file_flag(), LargeFileFlag::Normal);
assert_eq!(nak_pdu.pdu_type(), PduType::FileDirective);
assert_eq!(
nak_pdu.file_directive_type(),
Some(FileDirectiveType::NakPdu),
);
assert_eq!(nak_pdu.transmission_mode(), TransmissionMode::Acknowledged);
assert_eq!(nak_pdu.direction(), Direction::TowardsSender);
assert_eq!(nak_pdu.source_id(), TEST_SRC_ID.into());
assert_eq!(nak_pdu.dest_id(), TEST_DEST_ID.into());
assert_eq!(nak_pdu.transaction_seq_num(), TEST_SEQ_NUM.into());
}
#[test]
fn test_seg_request_api() {
let seg_req = SegmentRequests::U32Pairs(&[]);
assert!(seg_req.is_empty());
let seg_req = SegmentRequests::U64Pairs(&[]);
assert!(seg_req.is_empty());
}
#[test]
fn test_basic_creator() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let nak_pdu = NakPduCreator::new_no_segment_requests(pdu_header, 0, 0)
.expect("creating NAK PDU creator failed");
assert_eq!(nak_pdu.start_of_scope(), 0);
assert_eq!(nak_pdu.end_of_scope(), 0);
assert_eq!(nak_pdu.segment_requests(), None);
assert_eq!(nak_pdu.num_segment_reqs(), 0);
check_generic_fields(&nak_pdu);
}
#[test]
fn test_serialization_empty() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let nak_pdu = NakPduCreator::new_no_segment_requests(pdu_header, 100, 300)
.expect("creating NAK PDU creator failed");
assert_eq!(nak_pdu.start_of_scope(), 100);
assert_eq!(nak_pdu.end_of_scope(), 300);
let mut buf: [u8; 64] = [0; 64];
nak_pdu
.write_to_bytes(&mut buf)
.expect("writing NAK PDU to buffer failed");
verify_raw_header(nak_pdu.pdu_header(), &buf);
let mut current_idx = nak_pdu.pdu_header().header_len();
assert_eq!(current_idx + 9, nak_pdu.len_written());
assert_eq!(buf[current_idx], FileDirectiveType::NakPdu as u8);
current_idx += 1;
let start_of_scope =
u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap());
assert_eq!(start_of_scope, 100);
current_idx += 4;
let end_of_scope =
u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap());
assert_eq!(end_of_scope, 300);
current_idx += 4;
assert_eq!(current_idx, nak_pdu.len_written());
}
#[test]
fn test_serialization_two_segments() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let nak_pdu = NakPduCreator::new(pdu_header, 100, 300, &[(0, 0), (32, 64)])
.expect("creating NAK PDU creator failed");
let mut buf: [u8; 64] = [0; 64];
nak_pdu
.write_to_bytes(&mut buf)
.expect("writing NAK PDU to buffer failed");
verify_raw_header(nak_pdu.pdu_header(), &buf);
let mut current_idx = nak_pdu.pdu_header().header_len();
assert_eq!(current_idx + 9 + 16, nak_pdu.len_written());
assert_eq!(buf[current_idx], FileDirectiveType::NakPdu as u8);
current_idx += 1;
let start_of_scope =
u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap());
assert_eq!(start_of_scope, 100);
current_idx += 4;
let end_of_scope =
u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap());
assert_eq!(end_of_scope, 300);
current_idx += 4;
let first_seg_start =
u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap());
assert_eq!(first_seg_start, 0);
current_idx += 4;
let first_seg_end =
u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap());
assert_eq!(first_seg_end, 0);
current_idx += 4;
let second_seg_start =
u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap());
assert_eq!(second_seg_start, 32);
current_idx += 4;
let second_seg_end =
u32::from_be_bytes(buf[current_idx..current_idx + 4].try_into().unwrap());
assert_eq!(second_seg_end, 64);
current_idx += 4;
assert_eq!(current_idx, nak_pdu.len_written());
}
#[test]
fn test_deserialization_empty() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let nak_pdu = NakPduCreator::new_no_segment_requests(pdu_header, 100, 300)
.expect("creating NAK PDU creator failed");
let mut buf: [u8; 64] = [0; 64];
nak_pdu
.write_to_bytes(&mut buf)
.expect("writing NAK PDU to buffer failed");
let nak_pdu_deser = NakPduReader::from_bytes(&buf).expect("deserializing NAK PDU failed");
assert_eq!(nak_pdu_deser, nak_pdu);
check_generic_fields(&nak_pdu_deser);
}
#[test]
fn test_deserialization_large_segments() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Large);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let nak_pdu =
NakPduCreator::new_large_file_size(pdu_header, 100, 300, &[(50, 100), (200, 300)])
.expect("creating NAK PDU creator failed");
let mut buf: [u8; 128] = [0; 128];
nak_pdu
.write_to_bytes(&mut buf)
.expect("writing NAK PDU to buffer failed");
let nak_pdu_deser = NakPduReader::from_bytes(&buf).expect("deserializing NAK PDU failed");
assert_eq!(nak_pdu_deser, nak_pdu);
assert_eq!(nak_pdu_deser.start_of_scope(), 100);
assert_eq!(nak_pdu_deser.end_of_scope(), 300);
assert_eq!(nak_pdu_deser.num_segment_reqs(), 2);
assert!(nak_pdu_deser
.get_large_segment_requests_iterator()
.is_some());
assert!(nak_pdu_deser
.get_normal_segment_requests_iterator()
.is_none());
assert_eq!(
nak_pdu_deser
.get_large_segment_requests_iterator()
.unwrap()
.count(),
2
);
for (idx, large_segments) in nak_pdu_deser
.get_large_segment_requests_iterator()
.unwrap()
.enumerate()
{
if idx == 0 {
assert_eq!(large_segments.0, 50);
assert_eq!(large_segments.1, 100);
} else {
assert_eq!(large_segments.0, 200);
assert_eq!(large_segments.1, 300);
}
}
}
#[test]
fn test_deserialization_normal_segments() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let nak_pdu = NakPduCreator::new(pdu_header, 100, 300, &[(50, 100), (200, 300)])
.expect("creating NAK PDU creator failed");
let mut buf: [u8; 128] = [0; 128];
nak_pdu
.write_to_bytes(&mut buf)
.expect("writing NAK PDU to buffer failed");
let nak_pdu_deser = NakPduReader::from_bytes(&buf).expect("deserializing NAK PDU failed");
assert_eq!(nak_pdu_deser, nak_pdu);
assert_eq!(nak_pdu_deser.start_of_scope(), 100);
assert_eq!(nak_pdu_deser.end_of_scope(), 300);
assert_eq!(nak_pdu_deser.num_segment_reqs(), 2);
assert!(nak_pdu_deser
.get_normal_segment_requests_iterator()
.is_some());
assert!(nak_pdu_deser
.get_large_segment_requests_iterator()
.is_none());
assert_eq!(
nak_pdu_deser
.get_normal_segment_requests_iterator()
.unwrap()
.count(),
2
);
for (idx, large_segments) in nak_pdu_deser
.get_normal_segment_requests_iterator()
.unwrap()
.enumerate()
{
if idx == 0 {
assert_eq!(large_segments.0, 50);
assert_eq!(large_segments.1, 100);
} else {
assert_eq!(large_segments.0, 200);
assert_eq!(large_segments.1, 300);
}
}
}
#[test]
fn test_empty_is_empty() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let nak_pdu_0 =
NakPduCreator::new(pdu_header, 100, 300, &[]).expect("creating NAK PDU creator failed");
let nak_pdu_1 = NakPduCreator::new_no_segment_requests(pdu_header, 100, 300)
.expect("creating NAK PDU creator failed");
assert_eq!(nak_pdu_0, nak_pdu_1);
// Assert the segment request is mapped to None.
assert!(nak_pdu_0.segment_requests().is_none());
}
#[test]
fn test_new_generic_invalid_input() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let u32_list = SegmentRequests::U32Pairs(&[(0, 50), (50, 100)]);
//let error = NakPduCreator::new_generic(pdu_header, 100, 300, Some(u32_list));
let error = NakPduCreator::new_generic(
pdu_header,
u32::MAX as u64 + 1,
u32::MAX as u64 + 2,
Some(u32_list),
);
assert!(error.is_err());
let error = error.unwrap_err();
if let PduError::InvalidStartOrEndOfScopeValue = error {
assert_eq!(
error.to_string(),
"invalid start or end of scope value for NAK PDU"
);
} else {
panic!("unexpected error {error}");
}
}
#[test]
fn test_target_buf_too_small() {
let pdu_conf = common_pdu_conf(CrcFlag::NoCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let nak_pdu = NakPduCreator::new_no_segment_requests(pdu_header, 100, 300)
.expect("creating NAK PDU creator failed");
assert_eq!(nak_pdu.start_of_scope(), 100);
assert_eq!(nak_pdu.end_of_scope(), 300);
let mut buf: [u8; 5] = [0; 5];
let error = nak_pdu.write_to_bytes(&mut buf);
assert!(error.is_err());
let e = error.unwrap_err();
match e {
PduError::ByteConversion(conv_error) => match conv_error {
ByteConversionError::ToSliceTooSmall { found, expected } => {
assert_eq!(expected, nak_pdu.len_written());
assert_eq!(found, 5);
}
_ => panic!("unexpected error {conv_error}"),
},
_ => panic!("unexpected error {e}"),
}
}
#[test]
fn test_with_crc() {
let pdu_conf = common_pdu_conf(CrcFlag::WithCrc, LargeFileFlag::Normal);
let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0);
let nak_pdu = NakPduCreator::new_no_segment_requests(pdu_header, 0, 0)
.expect("creating NAK PDU creator failed");
let mut nak_vec = nak_pdu.to_vec().expect("writing NAK to vector failed");
assert_eq!(nak_vec.len(), pdu_header.header_len() + 9 + 2);
assert_eq!(nak_vec.len(), nak_pdu.len_written());
let nak_pdu_deser = NakPduReader::new(&nak_vec).expect("reading NAK PDU failed");
assert_eq!(nak_pdu_deser, nak_pdu);
nak_vec[nak_pdu.len_written() - 1] -= 1;
let nak_pdu_deser = NakPduReader::new(&nak_vec);
assert!(nak_pdu_deser.is_err());
if let Err(PduError::Checksum(raw)) = nak_pdu_deser {
assert_eq!(
raw,
u16::from_be_bytes(nak_vec[nak_pdu.len_written() - 2..].try_into().unwrap())
);
}
}
}

View File

@ -1,765 +0,0 @@
//! Generic CFDP type-length-value (TLV) abstraction as specified in CFDP 5.1.9.
use crate::cfdp::lv::{
generic_len_check_data_serialization, generic_len_check_deserialization, Lv, MIN_LV_LEN,
};
use crate::cfdp::TlvLvError;
use crate::util::{UnsignedByteField, UnsignedByteFieldError, UnsignedEnum};
use crate::{ByteConversionError, SizeMissmatch};
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub const MIN_TLV_LEN: usize = 2;
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(u8)]
pub enum TlvType {
FilestoreRequest = 0x00,
FilestoreResponse = 0x01,
MsgToUser = 0x02,
FaultHandler = 0x04,
FlowLabel = 0x05,
EntityId = 0x06,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TlvTypeField {
Standard(TlvType),
Custom(u8),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(u8)]
pub enum FilestoreActionCode {
CreateFile = 0b0000,
DeleteFile = 0b0001,
RenameFile = 0b0010,
/// This operation appends one file to another. The first specified name will form the first
/// part of the new file and the name of the new file. This function can be used to get
/// similar functionality to the UNIX cat utility (albeit for only two files).
AppendFile = 0b0011,
/// This operation replaces the content of the first specified file with the content of
/// the secondly specified file.
ReplaceFile = 0b0100,
CreateDirectory = 0b0101,
RemoveDirectory = 0b0110,
DenyFile = 0b0111,
DenyDirectory = 0b1000,
}
impl From<u8> for TlvTypeField {
fn from(value: u8) -> Self {
match TlvType::try_from(value) {
Ok(tlv_type) => TlvTypeField::Standard(tlv_type),
Err(_) => TlvTypeField::Custom(value),
}
}
}
impl From<TlvTypeField> for u8 {
fn from(value: TlvTypeField) -> Self {
match value {
TlvTypeField::Standard(std) => std as u8,
TlvTypeField::Custom(custom) => custom,
}
}
}
/// Generic CFDP type-length-value (TLV) abstraction as specified in CFDP 5.1.9.
///
/// # Lifetimes
/// * `data`: If the TLV is generated from a raw bytestream, this will be the lifetime of
/// the raw bytestream. If the TLV is generated from a raw slice or a similar data reference,
/// this will be the lifetime of that data reference.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Tlv<'data> {
tlv_type_field: TlvTypeField,
#[cfg_attr(feature = "serde", serde(borrow))]
lv: Lv<'data>,
}
impl<'data> Tlv<'data> {
pub fn new(tlv_type: TlvType, data: &[u8]) -> Result<Tlv, TlvLvError> {
Ok(Tlv {
tlv_type_field: TlvTypeField::Standard(tlv_type),
lv: Lv::new(data)?,
})
}
/// Creates a TLV with an empty value field.
pub fn new_empty(tlv_type: TlvType) -> Tlv<'data> {
Tlv {
tlv_type_field: TlvTypeField::Standard(tlv_type),
lv: Lv::new_empty(),
}
}
pub fn tlv_type_field(&self) -> TlvTypeField {
self.tlv_type_field
}
pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
generic_len_check_data_serialization(buf, self.len_value(), MIN_TLV_LEN)?;
buf[0] = self.tlv_type_field.into();
self.lv.write_to_be_bytes_no_len_check(&mut buf[1..]);
Ok(self.len_full())
}
pub fn value(&self) -> Option<&[u8]> {
self.lv.value()
}
/// Returns the length of the value part, not including the length byte.
pub fn len_value(&self) -> usize {
self.lv.len_value()
}
/// Returns the full raw length, including the length byte.
pub fn len_full(&self) -> usize {
self.lv.len_full() + 1
}
/// Checks whether the value field is empty.
pub fn is_empty(&self) -> bool {
self.lv.is_empty()
}
/// Creates a TLV give a raw bytestream. Please note that is is not necessary to pass the
/// bytestream with the exact size of the expected TLV. This function will take care
/// of parsing the length byte, and the length of the parsed TLV can be retrieved using
/// [Self::len_full].
pub fn from_bytes(buf: &'data [u8]) -> Result<Tlv<'data>, TlvLvError> {
generic_len_check_deserialization(buf, MIN_TLV_LEN)?;
Ok(Self {
tlv_type_field: TlvTypeField::from(buf[0]),
lv: Lv::from_bytes(&buf[MIN_LV_LEN..])?,
})
}
}
pub(crate) fn verify_tlv_type(raw_type: u8, expected_tlv_type: TlvType) -> Result<(), TlvLvError> {
let tlv_type = TlvType::try_from(raw_type)
.map_err(|_| TlvLvError::InvalidTlvTypeField((raw_type, Some(expected_tlv_type as u8))))?;
if tlv_type != expected_tlv_type {
return Err(TlvLvError::InvalidTlvTypeField((
tlv_type as u8,
Some(expected_tlv_type as u8),
)));
}
Ok(())
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct EntityIdTlv {
entity_id: UnsignedByteField,
}
impl EntityIdTlv {
pub fn new(entity_id: UnsignedByteField) -> Self {
Self { entity_id }
}
fn len_check(buf: &[u8]) -> Result<(), ByteConversionError> {
if buf.len() < 2 {
return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
found: buf.len(),
expected: 2,
}));
}
Ok(())
}
pub fn len_value(&self) -> usize {
self.entity_id.size()
}
pub fn len_full(&self) -> usize {
2 + self.entity_id.size()
}
pub fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
Self::len_check(buf)?;
buf[0] = TlvType::EntityId as u8;
buf[1] = self.entity_id.size() as u8;
self.entity_id.write_to_be_bytes(&mut buf[2..])
}
pub fn from_bytes(buf: &[u8]) -> Result<Self, TlvLvError> {
Self::len_check(buf)?;
verify_tlv_type(buf[0], TlvType::EntityId)?;
let len = buf[1];
if len != 1 && len != 2 && len != 4 && len != 8 {
return Err(TlvLvError::InvalidValueLength(len as usize));
}
// Okay to unwrap here. The checks before make sure that the deserialization never fails
let entity_id = UnsignedByteField::new_from_be_bytes(len as usize, &buf[2..]).unwrap();
Ok(Self { entity_id })
}
/// Convert to a generic [Tlv], which also erases the programmatic type information.
pub fn to_tlv(self, buf: &mut [u8]) -> Result<Tlv, ByteConversionError> {
Self::len_check(buf)?;
self.entity_id
.write_to_be_bytes(&mut buf[2..2 + self.entity_id.size()])?;
Tlv::new(TlvType::EntityId, &buf[2..2 + self.entity_id.size()]).map_err(|e| match e {
TlvLvError::ByteConversionError(e) => e,
// All other errors are impossible.
_ => panic!("unexpected TLV error"),
})
}
}
impl<'data> TryFrom<Tlv<'data>> for EntityIdTlv {
type Error = TlvLvError;
fn try_from(value: Tlv) -> Result<Self, Self::Error> {
match value.tlv_type_field {
TlvTypeField::Standard(tlv_type) => {
if tlv_type != TlvType::EntityId {
return Err(TlvLvError::InvalidTlvTypeField((
tlv_type as u8,
Some(TlvType::EntityId as u8),
)));
}
}
TlvTypeField::Custom(val) => {
return Err(TlvLvError::InvalidTlvTypeField((
val,
Some(TlvType::EntityId as u8),
)));
}
}
if value.len_value() != 1
&& value.len_value() != 2
&& value.len_value() != 4
&& value.len_value() != 8
{
return Err(TlvLvError::InvalidValueLength(value.len_value()));
}
Ok(Self::new(
UnsignedByteField::new_from_be_bytes(value.len_value(), value.value().unwrap())
.map_err(|e| match e {
UnsignedByteFieldError::ByteConversionError(e) => e,
// This can not happen, we checked for the length validity, and the data is always smaller than
// 255 bytes.
_ => panic!("unexpected error"),
})?,
))
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FilestoreRequestTlv<'first_name, 'second_name> {
action_code: FilestoreActionCode,
#[cfg_attr(feature = "serde", serde(borrow))]
first_name: Lv<'first_name>,
#[cfg_attr(feature = "serde", serde(borrow))]
second_name: Option<Lv<'second_name>>,
}
impl<'first_name, 'second_name> FilestoreRequestTlv<'first_name, 'second_name> {
pub fn new_create_file(first_name: Lv<'first_name>) -> Result<Self, TlvLvError> {
Self::new(FilestoreActionCode::CreateFile, first_name, None)
}
pub fn new_delete_file(first_name: Lv<'first_name>) -> Result<Self, TlvLvError> {
Self::new(FilestoreActionCode::DeleteFile, first_name, None)
}
pub fn new_rename_file(
source_name: Lv<'first_name>,
target_name: Lv<'second_name>,
) -> Result<Self, TlvLvError> {
Self::new(
FilestoreActionCode::RenameFile,
source_name,
Some(target_name),
)
}
/// This operation appends one file to another. The first specified name will form the first
/// part of the new file and the name of the new file. This function can be used to get
/// similar functionality to the UNIX cat utility (albeit for only two files).
pub fn new_append_file(
first_file: Lv<'first_name>,
second_file: Lv<'second_name>,
) -> Result<Self, TlvLvError> {
Self::new(
FilestoreActionCode::AppendFile,
first_file,
Some(second_file),
)
}
/// This operation replaces the content of the first specified file with the content of
/// the secondly specified file. This function can be used to get similar functionality to
/// the UNIX copy (cp) utility if the target file already exists.
pub fn new_replace_file(
replaced_file: Lv<'first_name>,
new_file: Lv<'second_name>,
) -> Result<Self, TlvLvError> {
Self::new(
FilestoreActionCode::ReplaceFile,
replaced_file,
Some(new_file),
)
}
pub fn new_create_directory(dir_name: Lv<'first_name>) -> Result<Self, TlvLvError> {
Self::new(FilestoreActionCode::CreateDirectory, dir_name, None)
}
pub fn new_remove_directory(dir_name: Lv<'first_name>) -> Result<Self, TlvLvError> {
Self::new(FilestoreActionCode::RemoveDirectory, dir_name, None)
}
pub fn new_deny_file(file_name: Lv<'first_name>) -> Result<Self, TlvLvError> {
Self::new(FilestoreActionCode::DenyFile, file_name, None)
}
pub fn new_deny_directory(dir_name: Lv<'first_name>) -> Result<Self, TlvLvError> {
Self::new(FilestoreActionCode::DenyDirectory, dir_name, None)
}
/// This function will return [None] if the respective action code requires two names but
/// only one is passed. It will also returns [None] if the cumulative length of the first
/// name and the second name exceeds 255 bytes.
///
/// This is the case for the rename, append and replace filestore request.
pub fn new(
action_code: FilestoreActionCode,
first_name: Lv<'first_name>,
second_name: Option<Lv<'second_name>>,
) -> Result<Self, TlvLvError> {
let mut base_value_len = first_name.len_full();
if Self::has_second_filename(action_code) {
if second_name.is_none() {
return Err(TlvLvError::SecondNameMissing);
}
base_value_len += second_name.as_ref().unwrap().len_full();
}
if base_value_len > u8::MAX as usize {
return Err(TlvLvError::InvalidValueLength(base_value_len));
}
Ok(Self {
action_code,
first_name,
second_name,
})
}
pub fn has_second_filename(action_code: FilestoreActionCode) -> bool {
if action_code == FilestoreActionCode::RenameFile
|| action_code == FilestoreActionCode::AppendFile
|| action_code == FilestoreActionCode::ReplaceFile
{
return true;
}
false
}
pub fn action_code(&self) -> FilestoreActionCode {
self.action_code
}
pub fn first_name(&self) -> Lv<'first_name> {
self.first_name
}
pub fn second_name(&self) -> Option<Lv<'second_name>> {
self.second_name
}
pub fn len_value(&self) -> usize {
let mut len = 1 + self.first_name.len_full();
if let Some(second_name) = self.second_name {
len += second_name.len_full();
}
len
}
pub fn len_full(&self) -> usize {
2 + self.len_value()
}
pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
if buf.len() < self.len_full() {
return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
found: buf.len(),
expected: self.len_full(),
}));
}
buf[0] = TlvType::FilestoreRequest as u8;
buf[1] = self.len_value() as u8;
buf[2] = (self.action_code as u8) << 4;
let mut current_idx = 3;
// Length checks were already performed.
self.first_name.write_to_be_bytes_no_len_check(
&mut buf[current_idx..current_idx + self.first_name.len_full()],
);
current_idx += self.first_name.len_full();
if let Some(second_name) = self.second_name {
second_name.write_to_be_bytes_no_len_check(
&mut buf[current_idx..current_idx + second_name.len_full()],
);
current_idx += second_name.len_full();
}
Ok(current_idx)
}
pub fn from_bytes<'longest: 'first_name + 'second_name>(
buf: &'longest [u8],
) -> Result<Self, TlvLvError> {
if buf.len() < 2 {
return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
found: buf.len(),
expected: 2,
})
.into());
}
verify_tlv_type(buf[0], TlvType::FilestoreRequest)?;
let len = buf[1] as usize;
let mut current_idx = 2;
let action_code = FilestoreActionCode::try_from((buf[2] >> 4) & 0b1111)
.map_err(|_| TlvLvError::InvalidFilestoreActionCode((buf[2] >> 4) & 0b1111))?;
current_idx += 1;
let first_name = Lv::from_bytes(&buf[current_idx..])?;
let mut second_name = None;
current_idx += first_name.len_full();
if Self::has_second_filename(action_code) {
if current_idx >= 2 + len {
return Err(TlvLvError::SecondNameMissing);
}
second_name = Some(Lv::from_bytes(&buf[current_idx..])?);
}
Ok(Self {
action_code,
first_name,
second_name,
})
}
}
#[cfg(test)]
mod tests {
use crate::cfdp::lv::Lv;
use crate::cfdp::tlv::{FilestoreActionCode, FilestoreRequestTlv, Tlv, TlvType, TlvTypeField};
use crate::cfdp::TlvLvError;
use crate::util::{UbfU8, UnsignedEnum};
const TLV_TEST_STR_0: &'static str = "hello.txt";
const TLV_TEST_STR_1: &'static str = "hello2.txt";
#[test]
fn test_basic() {
let entity_id = UbfU8::new(5);
let mut buf: [u8; 4] = [0; 4];
assert!(entity_id.write_to_be_bytes(&mut buf).is_ok());
let tlv_res = Tlv::new(TlvType::EntityId, &buf[0..1]);
assert!(tlv_res.is_ok());
let tlv_res = tlv_res.unwrap();
assert_eq!(
tlv_res.tlv_type_field(),
TlvTypeField::Standard(TlvType::EntityId)
);
assert_eq!(tlv_res.len_full(), 3);
assert_eq!(tlv_res.len_value(), 1);
assert!(!tlv_res.is_empty());
assert!(tlv_res.value().is_some());
assert_eq!(tlv_res.value().unwrap()[0], 5);
}
#[test]
fn test_serialization() {
let entity_id = UbfU8::new(5);
let mut buf: [u8; 4] = [0; 4];
assert!(entity_id.write_to_be_bytes(&mut buf).is_ok());
let tlv_res = Tlv::new(TlvType::EntityId, &buf[0..1]);
assert!(tlv_res.is_ok());
let tlv_res = tlv_res.unwrap();
let mut ser_buf: [u8; 4] = [0; 4];
assert!(tlv_res.write_to_bytes(&mut ser_buf).is_ok());
assert_eq!(ser_buf[0], TlvType::EntityId as u8);
assert_eq!(ser_buf[1], 1);
assert_eq!(ser_buf[2], 5);
}
#[test]
fn test_deserialization() {
let entity_id = UbfU8::new(5);
let mut buf: [u8; 4] = [0; 4];
assert!(entity_id.write_to_be_bytes(&mut buf[2..]).is_ok());
buf[0] = TlvType::EntityId as u8;
buf[1] = 1;
let tlv_from_raw = Tlv::from_bytes(&mut buf);
assert!(tlv_from_raw.is_ok());
let tlv_from_raw = tlv_from_raw.unwrap();
assert_eq!(
tlv_from_raw.tlv_type_field(),
TlvTypeField::Standard(TlvType::EntityId)
);
assert_eq!(tlv_from_raw.len_value(), 1);
assert_eq!(tlv_from_raw.len_full(), 3);
assert!(tlv_from_raw.value().is_some());
assert_eq!(tlv_from_raw.value().unwrap()[0], 5);
}
#[test]
fn test_empty() {
let tlv_empty = Tlv::new_empty(TlvType::MsgToUser);
assert!(tlv_empty.value().is_none());
assert!(tlv_empty.is_empty());
assert_eq!(tlv_empty.len_full(), 2);
assert_eq!(tlv_empty.len_value(), 0);
assert_eq!(
tlv_empty.tlv_type_field(),
TlvTypeField::Standard(TlvType::MsgToUser)
);
}
#[test]
fn test_empty_serialization() {
let tlv_empty = Tlv::new_empty(TlvType::MsgToUser);
let mut buf: [u8; 4] = [0; 4];
assert!(tlv_empty.write_to_bytes(&mut buf).is_ok());
assert_eq!(buf[0], TlvType::MsgToUser as u8);
assert_eq!(buf[1], 0);
}
#[test]
fn test_empty_deserialization() {
let mut buf: [u8; 4] = [0; 4];
buf[0] = TlvType::MsgToUser as u8;
buf[1] = 0;
let tlv_empty = Tlv::from_bytes(&mut buf);
assert!(tlv_empty.is_ok());
let tlv_empty = tlv_empty.unwrap();
assert!(tlv_empty.is_empty());
assert!(tlv_empty.value().is_none());
assert_eq!(
tlv_empty.tlv_type_field(),
TlvTypeField::Standard(TlvType::MsgToUser)
);
assert_eq!(tlv_empty.len_full(), 2);
assert_eq!(tlv_empty.len_value(), 0);
}
#[test]
fn test_buf_too_large() {
let buf_too_large: [u8; u8::MAX as usize + 1] = [0; u8::MAX as usize + 1];
let tlv_res = Tlv::new(TlvType::MsgToUser, &buf_too_large);
assert!(tlv_res.is_err());
let error = tlv_res.unwrap_err();
if let TlvLvError::DataTooLarge(size) = error {
assert_eq!(size, u8::MAX as usize + 1);
} else {
panic!("unexpected error {:?}", error);
}
}
#[test]
fn test_deserialization_custom_tlv_type() {
let mut buf: [u8; 4] = [0; 4];
buf[0] = 3;
buf[1] = 1;
buf[2] = 5;
let tlv = Tlv::from_bytes(&mut buf);
assert!(tlv.is_ok());
let tlv = tlv.unwrap();
assert_eq!(tlv.tlv_type_field(), TlvTypeField::Custom(3));
assert_eq!(tlv.len_value(), 1);
assert_eq!(tlv.len_full(), 3);
}
fn generic_fs_request_test_one_file(
action_code: FilestoreActionCode,
) -> FilestoreRequestTlv<'static, 'static> {
assert!(!FilestoreRequestTlv::has_second_filename(action_code));
let first_name = Lv::new_from_str(TLV_TEST_STR_0).unwrap();
let fs_request = match action_code {
FilestoreActionCode::CreateFile => FilestoreRequestTlv::new_create_file(first_name),
FilestoreActionCode::DeleteFile => FilestoreRequestTlv::new_delete_file(first_name),
FilestoreActionCode::CreateDirectory => {
FilestoreRequestTlv::new_create_directory(first_name)
}
FilestoreActionCode::RemoveDirectory => {
FilestoreRequestTlv::new_remove_directory(first_name)
}
FilestoreActionCode::DenyFile => FilestoreRequestTlv::new_deny_file(first_name),
FilestoreActionCode::DenyDirectory => {
FilestoreRequestTlv::new_deny_directory(first_name)
}
_ => panic!("invalid action code"),
};
assert!(fs_request.is_ok());
let fs_request = fs_request.unwrap();
assert_eq!(fs_request.len_value(), 1 + first_name.len_full());
assert_eq!(fs_request.len_full(), fs_request.len_value() + 2);
assert_eq!(fs_request.action_code(), action_code);
assert_eq!(fs_request.first_name(), first_name);
assert_eq!(fs_request.second_name(), None);
fs_request
}
fn generic_fs_request_test_two_files(
action_code: FilestoreActionCode,
) -> FilestoreRequestTlv<'static, 'static> {
assert!(FilestoreRequestTlv::has_second_filename(action_code));
let first_name = Lv::new_from_str(TLV_TEST_STR_0).unwrap();
let second_name = Lv::new_from_str(TLV_TEST_STR_1).unwrap();
let fs_request = match action_code {
FilestoreActionCode::ReplaceFile => {
FilestoreRequestTlv::new_replace_file(first_name, second_name)
}
FilestoreActionCode::AppendFile => {
FilestoreRequestTlv::new_append_file(first_name, second_name)
}
FilestoreActionCode::RenameFile => {
FilestoreRequestTlv::new_rename_file(first_name, second_name)
}
_ => panic!("invalid action code"),
};
assert!(fs_request.is_ok());
let fs_request = fs_request.unwrap();
assert_eq!(
fs_request.len_value(),
1 + first_name.len_full() + second_name.len_full()
);
assert_eq!(fs_request.len_full(), fs_request.len_value() + 2);
assert_eq!(fs_request.action_code(), action_code);
assert_eq!(fs_request.first_name(), first_name);
assert!(fs_request.second_name().is_some());
assert_eq!(fs_request.second_name().unwrap(), second_name);
fs_request
}
#[test]
fn test_fs_request_basic_create_file() {
generic_fs_request_test_one_file(FilestoreActionCode::CreateFile);
}
#[test]
fn test_fs_request_basic_delete() {
generic_fs_request_test_one_file(FilestoreActionCode::DeleteFile);
}
#[test]
fn test_fs_request_basic_create_dir() {
generic_fs_request_test_one_file(FilestoreActionCode::CreateDirectory);
}
#[test]
fn test_fs_request_basic_remove_dir() {
generic_fs_request_test_one_file(FilestoreActionCode::RemoveDirectory);
}
#[test]
fn test_fs_request_basic_deny_file() {
generic_fs_request_test_one_file(FilestoreActionCode::DenyFile);
}
#[test]
fn test_fs_request_basic_deny_dir() {
generic_fs_request_test_one_file(FilestoreActionCode::DenyDirectory);
}
#[test]
fn test_fs_request_basic_append_file() {
generic_fs_request_test_two_files(FilestoreActionCode::AppendFile);
}
#[test]
fn test_fs_request_basic_rename_file() {
generic_fs_request_test_two_files(FilestoreActionCode::RenameFile);
}
#[test]
fn test_fs_request_basic_replace_file() {
generic_fs_request_test_two_files(FilestoreActionCode::ReplaceFile);
}
fn check_fs_request_first_part(
buf: &[u8],
action_code: FilestoreActionCode,
expected_val_len: u8,
) -> usize {
assert_eq!(buf[0], TlvType::FilestoreRequest as u8);
assert_eq!(buf[1], expected_val_len);
assert_eq!((buf[2] >> 4) & 0b1111, action_code as u8);
let lv = Lv::from_bytes(&buf[3..]);
assert!(lv.is_ok());
let lv = lv.unwrap();
assert_eq!(lv.value_as_str().unwrap().unwrap(), TLV_TEST_STR_0);
3 + lv.len_full()
}
#[test]
fn test_fs_request_serialization_one_file() {
let req = generic_fs_request_test_one_file(FilestoreActionCode::CreateFile);
let mut buf: [u8; 64] = [0; 64];
let res = req.write_to_bytes(&mut buf);
assert!(res.is_ok());
let written = res.unwrap();
assert_eq!(written, 3 + 1 + TLV_TEST_STR_0.len());
assert_eq!(written, req.len_full());
check_fs_request_first_part(
&buf,
FilestoreActionCode::CreateFile,
1 + 1 + TLV_TEST_STR_0.len() as u8,
);
}
#[test]
fn test_fs_request_deserialization_one_file() {
let req = generic_fs_request_test_one_file(FilestoreActionCode::CreateFile);
let mut buf: [u8; 64] = [0; 64];
let res = req.write_to_bytes(&mut buf);
assert!(res.is_ok());
let req_conv_back = FilestoreRequestTlv::from_bytes(&buf);
assert!(req_conv_back.is_ok());
let req_conv_back = req_conv_back.unwrap();
assert_eq!(req_conv_back, req);
}
#[test]
fn test_fs_request_serialization_two_files() {
let req = generic_fs_request_test_two_files(FilestoreActionCode::RenameFile);
let mut buf: [u8; 64] = [0; 64];
let res = req.write_to_bytes(&mut buf);
assert!(res.is_ok());
let written = res.unwrap();
assert_eq!(written, req.len_full());
assert_eq!(
written,
3 + 1 + TLV_TEST_STR_0.len() + 1 + TLV_TEST_STR_1.len()
);
let current_idx = check_fs_request_first_part(
&buf,
FilestoreActionCode::RenameFile,
1 + 1 + TLV_TEST_STR_0.len() as u8 + 1 + TLV_TEST_STR_1.len() as u8,
);
let second_lv = Lv::from_bytes(&buf[current_idx..]);
assert!(second_lv.is_ok());
let second_lv = second_lv.unwrap();
assert_eq!(second_lv.value_as_str().unwrap().unwrap(), TLV_TEST_STR_1);
assert_eq!(current_idx + second_lv.len_full(), req.len_full());
}
#[test]
fn test_fs_request_deserialization_two_files() {
let req = generic_fs_request_test_two_files(FilestoreActionCode::RenameFile);
let mut buf: [u8; 64] = [0; 64];
req.write_to_bytes(&mut buf).unwrap();
let req_conv_back = FilestoreRequestTlv::from_bytes(&buf);
assert!(req_conv_back.is_ok());
let req_conv_back = req_conv_back.unwrap();
assert_eq!(req_conv_back, req);
}
}

1475
src/cfdp/tlv/mod.rs Normal file

File diff suppressed because it is too large Load Diff

224
src/cfdp/tlv/msg_to_user.rs Normal file
View File

@ -0,0 +1,224 @@
//! Abstractions for the Message to User CFDP TLV subtype.
#[cfg(feature = "alloc")]
use super::TlvOwned;
use super::{GenericTlv, ReadableTlv, Tlv, TlvLvError, TlvType, TlvTypeField, WritableTlv};
use crate::{cfdp::{InvalidTlvTypeFieldError, TlvLvDataTooLargeError}, ByteConversionError};
use delegate::delegate;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct MsgToUserTlv<'data> {
pub tlv: Tlv<'data>,
}
impl<'data> MsgToUserTlv<'data> {
/// Create a new message to user TLV where the type field is set correctly.
pub fn new(value: &'data [u8]) -> Result<MsgToUserTlv<'data>, TlvLvDataTooLargeError> {
Ok(Self {
tlv: Tlv::new(TlvType::MsgToUser, value)?,
})
}
delegate! {
to self.tlv {
pub fn value(&self) -> &[u8];
/// Helper method to retrieve the length of the value. Simply calls the [slice::len] method of
/// [Self::value]
pub fn len_value(&self) -> usize;
/// Returns the full raw length, including the length byte.
pub fn len_full(&self) -> usize;
/// Checks whether the value field is empty.
pub fn is_empty(&self) -> bool;
/// If the TLV was generated from a raw bytestream using [Self::from_bytes], the raw start
/// of the TLV can be retrieved with this method.
pub fn raw_data(&self) -> Option<&[u8]>;
}
}
pub fn is_standard_tlv(&self) -> bool {
true
}
pub fn tlv_type(&self) -> Option<TlvType> {
Some(TlvType::MsgToUser)
}
/// Check whether this message is a reserved CFDP message like a Proxy Operation Message.
pub fn is_reserved_cfdp_msg(&self) -> bool {
if self.value().len() < 4 {
return false;
}
let value = self.value();
if value[0] == b'c' && value[1] == b'f' && value[2] == b'd' && value[3] == b'p' {
return true;
}
false
}
/// This is a thin wrapper around [Tlv::from_bytes] with the additional type check.
pub fn from_bytes(buf: &'data [u8]) -> Result<MsgToUserTlv<'data>, TlvLvError> {
let msg_to_user = Self {
tlv: Tlv::from_bytes(buf)?,
};
match msg_to_user.tlv.tlv_type_field() {
TlvTypeField::Standard(tlv_type) => {
if tlv_type != TlvType::MsgToUser {
return Err(InvalidTlvTypeFieldError {
found: tlv_type as u8,
expected: Some(TlvType::MsgToUser as u8),
}.into());
}
}
TlvTypeField::Custom(raw) => {
return Err(InvalidTlvTypeFieldError {
found: raw,
expected: Some(TlvType::MsgToUser as u8),
}.into());
}
}
Ok(msg_to_user)
}
pub fn to_tlv(&self) -> Tlv<'data> {
self.tlv
}
#[cfg(feature = "alloc")]
pub fn to_owned(&self) -> TlvOwned {
self.tlv.to_owned()
}
}
impl<'a> From<MsgToUserTlv<'a>> for Tlv<'a> {
fn from(value: MsgToUserTlv<'a>) -> Tlv<'a> {
value.to_tlv()
}
}
impl WritableTlv for MsgToUserTlv<'_> {
fn len_written(&self) -> usize {
self.len_full()
}
delegate!(
to self.tlv {
fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError>;
}
);
}
impl GenericTlv for MsgToUserTlv<'_> {
fn tlv_type_field(&self) -> TlvTypeField {
self.tlv.tlv_type_field()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
let custom_value: [u8; 4] = [1, 2, 3, 4];
let msg_to_user = MsgToUserTlv::new(&custom_value);
assert!(msg_to_user.is_ok());
let msg_to_user = msg_to_user.unwrap();
assert!(msg_to_user.is_standard_tlv());
assert_eq!(msg_to_user.tlv_type().unwrap(), TlvType::MsgToUser);
assert_eq!(
msg_to_user.tlv_type_field(),
TlvTypeField::Standard(TlvType::MsgToUser)
);
assert_eq!(msg_to_user.value(), custom_value);
assert_eq!(msg_to_user.value().len(), 4);
assert_eq!(msg_to_user.len_value(), 4);
assert_eq!(msg_to_user.len_full(), 6);
assert!(!msg_to_user.is_empty());
assert!(msg_to_user.raw_data().is_none());
assert!(!msg_to_user.is_reserved_cfdp_msg());
}
#[test]
fn test_reserved_msg_serialization() {
let custom_value: [u8; 4] = [1, 2, 3, 4];
let msg_to_user = MsgToUserTlv::new(&custom_value).unwrap();
let mut buf: [u8; 6] = [0; 6];
msg_to_user.write_to_bytes(&mut buf).unwrap();
assert_eq!(
buf,
[
TlvType::MsgToUser as u8,
custom_value.len() as u8,
1,
2,
3,
4
]
);
}
#[test]
fn test_msg_to_user_type_reduction() {
let custom_value: [u8; 4] = [1, 2, 3, 4];
let msg_to_user = MsgToUserTlv::new(&custom_value).unwrap();
let tlv = msg_to_user.to_tlv();
assert_eq!(
tlv.tlv_type_field(),
TlvTypeField::Standard(TlvType::MsgToUser)
);
assert_eq!(tlv.value(), custom_value);
}
#[test]
fn test_msg_to_user_to_tlv() {
let custom_value: [u8; 4] = [1, 2, 3, 4];
let msg_to_user = MsgToUserTlv::new(&custom_value).unwrap();
let tlv: Tlv = msg_to_user.into();
assert_eq!(msg_to_user.to_tlv(), tlv);
}
#[test]
fn test_msg_to_user_owner_converter() {
let custom_value: [u8; 4] = [1, 2, 3, 4];
let msg_to_user = MsgToUserTlv::new(&custom_value).unwrap();
let tlv = msg_to_user.to_owned();
assert_eq!(
tlv.tlv_type_field(),
TlvTypeField::Standard(TlvType::MsgToUser)
);
assert_eq!(tlv.value(), custom_value);
}
#[test]
fn test_reserved_msg_deserialization() {
let custom_value: [u8; 3] = [1, 2, 3];
let msg_to_user = MsgToUserTlv::new(&custom_value).unwrap();
let msg_to_user_vec = msg_to_user.to_vec();
let msg_to_user_from_bytes = MsgToUserTlv::from_bytes(&msg_to_user_vec).unwrap();
assert!(!msg_to_user.is_reserved_cfdp_msg());
assert_eq!(msg_to_user_from_bytes, msg_to_user);
assert_eq!(msg_to_user_from_bytes.value(), msg_to_user.value());
assert_eq!(msg_to_user_from_bytes.tlv_type(), msg_to_user.tlv_type());
}
#[test]
fn test_reserved_msg_deserialization_invalid_type() {
let trash: [u8; 5] = [TlvType::FlowLabel as u8, 3, 1, 2, 3];
let error = MsgToUserTlv::from_bytes(&trash).unwrap_err();
if let TlvLvError::InvalidTlvTypeField(inner) = error {
assert_eq!(inner.found, TlvType::FlowLabel as u8);
assert_eq!(inner.expected, Some(TlvType::MsgToUser as u8));
} else {
panic!("Wrong error type returned: {:?}", error);
}
}
#[test]
fn test_reserved_msg() {
let reserved_str = "cfdp";
let msg_to_user = MsgToUserTlv::new(reserved_str.as_bytes());
assert!(msg_to_user.is_ok());
let msg_to_user = msg_to_user.unwrap();
assert!(msg_to_user.is_reserved_cfdp_msg());
}
}

View File

@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Eq, PartialEq, Copy, Clone, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum Subservice {
// Regular HK
@ -29,3 +30,33 @@ pub enum Subservice {
TcGenerateOneShotDiag = 28,
TcModifyDiagCollectionInterval = 32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_try_from_u8() {
let hk_report_subservice_raw = 25;
let hk_report: Subservice = Subservice::try_from(hk_report_subservice_raw).unwrap();
assert_eq!(hk_report, Subservice::TmHkPacket);
}
#[test]
fn test_into_u8() {
let hk_report_raw: u8 = Subservice::TmHkPacket.into();
assert_eq!(hk_report_raw, 25);
}
#[test]
fn test_partial_eq() {
let hk_report_raw = Subservice::TmHkPacket;
assert_ne!(hk_report_raw, Subservice::TcGenerateOneShotHk);
assert_eq!(hk_report_raw, Subservice::TmHkPacket);
}
#[test]
fn test_copy_clone() {
let hk_report = Subservice::TmHkPacket;
let hk_report_copy = hk_report;
assert_eq!(hk_report, hk_report_copy);
}
}

View File

@ -1,16 +1,16 @@
//! Common definitions and helpers required to create PUS TMTC packets according to
//! [ECSS-E-ST-70-41C](https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/)
//!
//! You can find the PUS telecommand definitions in the [tc] module and ithe PUS telemetry definitions
//! inside the [tm] module.
//! You can find the PUS telecommand types in the [tc] module and the the PUS telemetry
//! types inside the [tm] module.
use crate::{ByteConversionError, CcsdsPacket, CRC_CCITT_FALSE};
use core::fmt::{Debug, Display, Formatter};
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use core::fmt::Debug;
use core::mem::size_of;
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "std")]
use std::error::Error;
pub mod event;
pub mod hk;
@ -20,7 +20,6 @@ pub mod tm;
pub mod verification;
pub type CrcType = u16;
pub const CCSDS_HEADER_LEN: usize = size_of::<crate::zc::SpHeader>();
#[derive(Debug, Copy, Clone, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@ -72,6 +71,8 @@ pub enum PusServiceId {
/// All PUS versions. Only PUS C is supported by this library.
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub enum PusVersion {
EsaPus = 0,
PusA = 1,
@ -93,8 +94,9 @@ impl TryFrom<u8> for PusVersion {
}
/// ECSS Packet Type Codes (PTC)s.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(u8)]
pub enum PacketTypeCodes {
Boolean = 1,
Enumerated = 2,
@ -113,9 +115,10 @@ pub enum PacketTypeCodes {
pub type Ptc = PacketTypeCodes;
/// ECSS Packet Field Codes (PFC)s for the unsigned [Ptc].
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum UnsignedPfc {
#[repr(u8)]
pub enum PfcUnsigned {
OneByte = 4,
TwelveBits = 8,
TwoBytes = 12,
@ -129,9 +132,10 @@ pub enum UnsignedPfc {
}
/// ECSS Packet Field Codes (PFC)s for the real (floating point) [Ptc].
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum RealPfc {
#[repr(u8)]
pub enum PfcReal {
/// 4 octets simple precision format (IEEE)
Float = 1,
/// 8 octets simple precision format (IEEE)
@ -142,60 +146,19 @@ pub enum RealPfc {
DoubleMilStd = 4,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum PusError {
#[error("PUS version {0:?} not supported")]
VersionNotSupported(PusVersion),
IncorrectCrc(u16),
RawDataTooShort(usize),
NoRawData,
#[error("checksum verification for crc16 {0:#06x} failed")]
ChecksumFailure(u16),
/// CRC16 needs to be calculated first
CrcCalculationMissing,
ByteConversion(ByteConversionError),
}
impl Display for PusError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
PusError::VersionNotSupported(v) => {
write!(f, "PUS version {v:?} not supported")
}
PusError::IncorrectCrc(crc) => {
write!(f, "crc16 {crc:#04x} is incorrect")
}
PusError::RawDataTooShort(size) => {
write!(
f,
"deserialization error, provided raw data with size {size} too short"
)
}
PusError::NoRawData => {
write!(f, "no raw data provided")
}
PusError::CrcCalculationMissing => {
write!(f, "crc16 was not calculated")
}
PusError::ByteConversion(e) => {
write!(f, "low level byte conversion error: {e}")
}
}
}
}
#[cfg(feature = "std")]
impl Error for PusError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
if let PusError::ByteConversion(e) = self {
return Some(e);
}
None
}
}
impl From<ByteConversionError> for PusError {
fn from(e: ByteConversionError) -> Self {
PusError::ByteConversion(e)
}
//#[error("crc16 was not calculated")]
//CrcCalculationMissing,
#[error("pus error: {0}")]
ByteConversion(#[from] ByteConversionError),
}
/// Generic trait to describe common attributes for both PUS Telecommands (TC) and PUS Telemetry
@ -210,9 +173,12 @@ pub trait PusPacket: CcsdsPacket {
fn crc16(&self) -> Option<u16>;
}
pub(crate) fn crc_from_raw_data(raw_data: &[u8]) -> Result<u16, PusError> {
pub(crate) fn crc_from_raw_data(raw_data: &[u8]) -> Result<u16, ByteConversionError> {
if raw_data.len() < 2 {
return Err(PusError::RawDataTooShort(raw_data.len()));
return Err(ByteConversionError::FromSliceTooSmall {
found: raw_data.len(),
expected: 2,
});
}
Ok(u16::from_be_bytes(
raw_data[raw_data.len() - 2..raw_data.len()]
@ -227,32 +193,16 @@ pub(crate) fn calc_pus_crc16(bytes: &[u8]) -> u16 {
digest.finalize()
}
pub(crate) fn crc_procedure(
calc_on_serialization: bool,
cached_crc16: &Option<u16>,
start_idx: usize,
curr_idx: usize,
slice: &[u8],
) -> Result<u16, PusError> {
let crc16;
if calc_on_serialization {
crc16 = calc_pus_crc16(&slice[start_idx..curr_idx])
} else if cached_crc16.is_none() {
return Err(PusError::CrcCalculationMissing);
} else {
crc16 = cached_crc16.unwrap();
}
Ok(crc16)
}
pub(crate) fn user_data_from_raw(
current_idx: usize,
total_len: usize,
raw_data_len: usize,
slice: &[u8],
) -> Result<&[u8], PusError> {
) -> Result<&[u8], ByteConversionError> {
match current_idx {
_ if current_idx > total_len - 2 => Err(PusError::RawDataTooShort(raw_data_len)),
_ if current_idx > total_len - 2 => Err(ByteConversionError::FromSliceTooSmall {
found: total_len - 2,
expected: current_idx,
}),
_ => Ok(&slice[current_idx..total_len - 2]),
}
}
@ -262,8 +212,8 @@ pub(crate) fn verify_crc16_ccitt_false_from_raw_to_pus_error(
crc16: u16,
) -> Result<(), PusError> {
verify_crc16_ccitt_false_from_raw(raw_data)
.then(|| ())
.ok_or(PusError::IncorrectCrc(crc16))
.then_some(())
.ok_or(PusError::ChecksumFailure(crc16))
}
pub(crate) fn verify_crc16_ccitt_false_from_raw(raw_data: &[u8]) -> bool {
@ -278,9 +228,13 @@ pub(crate) fn verify_crc16_ccitt_false_from_raw(raw_data: &[u8]) -> bool {
macro_rules! ccsds_impl {
() => {
delegate!(to self.sp_header {
#[inline]
fn ccsds_version(&self) -> u8;
#[inline]
fn packet_id(&self) -> crate::PacketId;
#[inline]
fn psc(&self) -> crate::PacketSequenceCtrl;
#[inline]
fn data_len(&self) -> u16;
});
}
@ -289,8 +243,11 @@ macro_rules! ccsds_impl {
macro_rules! sp_header_impls {
() => {
delegate!(to self.sp_header {
#[inline]
pub fn set_apid(&mut self, apid: u16) -> bool;
#[inline]
pub fn set_seq_count(&mut self, seq_count: u16) -> bool;
#[inline]
pub fn set_seq_flags(&mut self, seq_flag: SequenceFlags);
});
}
@ -313,15 +270,19 @@ pub trait EcssEnumerationExt: EcssEnumeration + Debug + Copy + Clone + PartialEq
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GenericEcssEnumWrapper<TYPE> {
pub struct GenericEcssEnumWrapper<TYPE: Copy + Into<u64>> {
field: GenericUnsignedByteField<TYPE>,
}
impl<TYPE> GenericEcssEnumWrapper<TYPE> {
impl<TYPE: Copy + Into<u64>> GenericEcssEnumWrapper<TYPE> {
pub const fn ptc() -> PacketTypeCodes {
PacketTypeCodes::Enumerated
}
pub const fn value_typed(&self) -> TYPE {
self.field.value_typed()
}
pub fn new(val: TYPE) -> Self {
Self {
field: GenericUnsignedByteField::new(val),
@ -329,7 +290,7 @@ impl<TYPE> GenericEcssEnumWrapper<TYPE> {
}
}
impl<TYPE: ToBeBytes> UnsignedEnum for GenericEcssEnumWrapper<TYPE> {
impl<TYPE: Copy + ToBeBytes + Into<u64>> UnsignedEnum for GenericEcssEnumWrapper<TYPE> {
fn size(&self) -> usize {
(self.pfc() / 8) as usize
}
@ -337,45 +298,97 @@ impl<TYPE: ToBeBytes> UnsignedEnum for GenericEcssEnumWrapper<TYPE> {
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
self.field.write_to_be_bytes(buf)
}
fn value(&self) -> u64 {
self.field.value()
}
}
impl<TYPE: ToBeBytes> EcssEnumeration for GenericEcssEnumWrapper<TYPE> {
impl<TYPE: Copy + ToBeBytes + Into<u64>> EcssEnumeration for GenericEcssEnumWrapper<TYPE> {
fn pfc(&self) -> u8 {
size_of::<TYPE>() as u8 * 8_u8
}
}
impl<TYPE: Debug + Copy + Clone + PartialEq + Eq + ToBeBytes> EcssEnumerationExt
impl<TYPE: Debug + Copy + Clone + PartialEq + Eq + ToBeBytes + Into<u64>> EcssEnumerationExt
for GenericEcssEnumWrapper<TYPE>
{
}
pub type EcssEnumU8 = GenericEcssEnumWrapper<u8>;
pub type EcssEnumU16 = GenericEcssEnumWrapper<u16>;
pub type EcssEnumU32 = GenericEcssEnumWrapper<u32>;
pub type EcssEnumU64 = GenericEcssEnumWrapper<u64>;
impl<T: Copy + Into<u64>> From<T> for GenericEcssEnumWrapper<T> {
fn from(value: T) -> Self {
Self::new(value)
}
}
macro_rules! generic_ecss_enum_typedefs_and_from_impls {
($($ty:ty => $Enum:ident),*) => {
$(
pub type $Enum = GenericEcssEnumWrapper<$ty>;
impl From<$Enum> for $ty {
fn from(value: $Enum) -> Self {
value.value_typed()
}
}
)*
};
}
// Generates EcssEnum<$TY> type definitions as well as a From<$TY> for EcssEnum<$TY>
// implementation.
generic_ecss_enum_typedefs_and_from_impls! {
u8 => EcssEnumU8,
u16 => EcssEnumU16,
u32 => EcssEnumU32,
u64 => EcssEnumU64
}
/// Generic trait for PUS packet abstractions which can written to a raw slice as their raw
/// byte representation. This is especially useful for generic abstractions which depend only
/// on the serialization of those packets.
pub trait SerializablePusPacket {
fn len_packed(&self) -> usize;
pub trait WritablePusPacket {
fn len_written(&self) -> usize;
fn write_to_bytes(&self, slice: &mut [u8]) -> Result<usize, PusError>;
#[cfg(feature = "alloc")]
fn to_vec(&self) -> Result<Vec<u8>, PusError> {
// This is the correct way to do this. See
// [this issue](https://github.com/rust-lang/rust-clippy/issues/4483) for caveats of more
// "efficient" implementations.
let mut vec = alloc::vec![0; self.len_written()];
self.write_to_bytes(&mut vec)?;
Ok(vec)
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use crate::ecss::{EcssEnumU16, EcssEnumU32, EcssEnumU8, UnsignedEnum};
use crate::ByteConversionError;
use super::*;
#[cfg(feature = "serde")]
use crate::tests::generic_serde_test;
#[test]
fn test_enum_u8() {
let mut buf = [0, 0, 0];
let my_enum = EcssEnumU8::new(1);
assert_eq!(EcssEnumU8::ptc(), Ptc::Enumerated);
assert_eq!(my_enum.size(), 1);
assert_eq!(my_enum.pfc(), 8);
my_enum
.write_to_be_bytes(&mut buf[1..2])
.expect("To byte conversion of u8 failed");
assert_eq!(buf[1], 1);
assert_eq!(my_enum.value(), 1);
assert_eq!(my_enum.value_typed(), 1);
let enum_as_u8: u8 = my_enum.into();
assert_eq!(enum_as_u8, 1);
let vec = my_enum.to_vec();
assert_eq!(vec, buf[1..2]);
}
#[test]
@ -385,8 +398,16 @@ mod tests {
my_enum
.write_to_be_bytes(&mut buf[1..3])
.expect("To byte conversion of u8 failed");
assert_eq!(my_enum.size(), 2);
assert_eq!(my_enum.pfc(), 16);
assert_eq!(buf[1], 0x1f);
assert_eq!(buf[2], 0x2f);
assert_eq!(my_enum.value(), 0x1f2f);
assert_eq!(my_enum.value_typed(), 0x1f2f);
let enum_as_raw: u16 = my_enum.into();
assert_eq!(enum_as_raw, 0x1f2f);
let vec = my_enum.to_vec();
assert_eq!(vec, buf[1..3]);
}
#[test]
@ -397,9 +418,9 @@ mod tests {
assert!(res.is_err());
let error = res.unwrap_err();
match error {
ByteConversionError::ToSliceTooSmall(missmatch) => {
assert_eq!(missmatch.expected, 2);
assert_eq!(missmatch.found, 1);
ByteConversionError::ToSliceTooSmall { found, expected } => {
assert_eq!(expected, 2);
assert_eq!(found, 1);
}
_ => {
panic!("Unexpected error {:?}", error);
@ -418,6 +439,12 @@ mod tests {
assert_eq!(buf[2], 0x2f);
assert_eq!(buf[3], 0x3f);
assert_eq!(buf[4], 0x4f);
assert_eq!(my_enum.value(), 0x1f2f3f4f);
assert_eq!(my_enum.value_typed(), 0x1f2f3f4f);
let enum_as_raw: u32 = my_enum.into();
assert_eq!(enum_as_raw, 0x1f2f3f4f);
let vec = my_enum.to_vec();
assert_eq!(vec, buf[1..5]);
}
#[test]
@ -428,13 +455,108 @@ mod tests {
assert!(res.is_err());
let error = res.unwrap_err();
match error {
ByteConversionError::ToSliceTooSmall(missmatch) => {
assert_eq!(missmatch.expected, 4);
assert_eq!(missmatch.found, 3);
ByteConversionError::ToSliceTooSmall { found, expected } => {
assert_eq!(expected, 4);
assert_eq!(found, 3);
}
_ => {
panic!("Unexpected error {:?}", error);
}
}
}
#[test]
fn test_enum_u64() {
let mut buf = [0; 8];
let my_enum = EcssEnumU64::new(0x1f2f3f4f5f);
my_enum
.write_to_be_bytes(&mut buf)
.expect("To byte conversion of u64 failed");
assert_eq!(buf[3], 0x1f);
assert_eq!(buf[4], 0x2f);
assert_eq!(buf[5], 0x3f);
assert_eq!(buf[6], 0x4f);
assert_eq!(buf[7], 0x5f);
assert_eq!(my_enum.value(), 0x1f2f3f4f5f);
assert_eq!(my_enum.value_typed(), 0x1f2f3f4f5f);
let enum_as_raw: u64 = my_enum.into();
assert_eq!(enum_as_raw, 0x1f2f3f4f5f);
assert_eq!(u64::from_be_bytes(buf), 0x1f2f3f4f5f);
let vec = my_enum.to_vec();
assert_eq!(vec, buf);
}
#[test]
fn test_pus_error_display() {
let unsupport_version = PusError::VersionNotSupported(super::PusVersion::EsaPus);
let write_str = unsupport_version.to_string();
assert_eq!(write_str, "PUS version EsaPus not supported")
}
#[test]
fn test_service_id_from_u8() {
let verification_id_raw = 1;
let verification_id = PusServiceId::try_from(verification_id_raw).unwrap();
assert_eq!(verification_id, PusServiceId::Verification);
}
#[test]
fn test_ptc_from_u8() {
let ptc_raw = Ptc::AbsoluteTime as u8;
let ptc = Ptc::try_from(ptc_raw).unwrap();
assert_eq!(ptc, Ptc::AbsoluteTime);
}
#[test]
fn test_unsigned_pfc_from_u8() {
let pfc_raw = PfcUnsigned::OneByte as u8;
let pfc = PfcUnsigned::try_from(pfc_raw).unwrap();
assert_eq!(pfc, PfcUnsigned::OneByte);
}
#[test]
fn test_real_pfc_from_u8() {
let pfc_raw = PfcReal::Double as u8;
let pfc = PfcReal::try_from(pfc_raw).unwrap();
assert_eq!(pfc, PfcReal::Double);
}
#[test]
fn test_pus_error_eq_impl() {
assert_eq!(
PusError::VersionNotSupported(PusVersion::EsaPus),
PusError::VersionNotSupported(PusVersion::EsaPus)
);
}
#[test]
fn test_pus_error_clonable() {
let pus_error = PusError::ChecksumFailure(0x0101);
let cloned = pus_error;
assert_eq!(pus_error, cloned);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_pus_service_id() {
generic_serde_test(PusServiceId::Verification);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_ptc() {
generic_serde_test(Ptc::AbsoluteTime);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_pfc_unsigned() {
generic_serde_test(PfcUnsigned::EightBytes);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_pfc_real() {
generic_serde_test(PfcReal::Double);
}
}

View File

@ -54,6 +54,7 @@ pub enum SchedStatus {
}
impl From<bool> for SchedStatus {
#[inline]
fn from(value: bool) -> Self {
if value {
SchedStatus::Enabled
@ -76,6 +77,8 @@ pub enum TimeWindowType {
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "serde")]
use crate::tests::generic_serde_test;
#[test]
fn test_bool_conv_0() {
@ -102,4 +105,22 @@ mod tests {
let subservice: Subservice = 22u8.try_into().unwrap();
assert_eq!(subservice, Subservice::TcCreateScheduleGroup);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_subservice_id() {
generic_serde_test(Subservice::TcEnableScheduling);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_sched_status() {
generic_serde_test(SchedStatus::Enabled);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_time_window_type() {
generic_serde_test(TimeWindowType::SelectAll);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

250
src/seq_count.rs Normal file
View File

@ -0,0 +1,250 @@
use crate::MAX_SEQ_COUNT;
use core::cell::Cell;
use paste::paste;
#[cfg(feature = "std")]
pub use stdmod::*;
/// Core trait for objects which can provide a sequence count.
///
/// The core functions are not mutable on purpose to allow easier usage with
/// static structs when using the interior mutability pattern. This can be achieved by using
/// [Cell], [core::cell::RefCell] or atomic types.
pub trait SequenceCountProvider {
type Raw: Into<u64>;
const MAX_BIT_WIDTH: usize;
fn get(&self) -> Self::Raw;
fn increment(&self);
fn get_and_increment(&self) -> Self::Raw {
let val = self.get();
self.increment();
val
}
}
#[derive(Clone)]
pub struct SeqCountProviderSimple<T: Copy> {
seq_count: Cell<T>,
max_val: T,
}
macro_rules! impl_for_primitives {
($($ty: ident,)+) => {
$(
paste! {
impl SeqCountProviderSimple<$ty> {
pub fn [<new_custom_max_val_ $ty>](max_val: $ty) -> Self {
Self {
seq_count: Cell::new(0),
max_val,
}
}
pub fn [<new_ $ty>]() -> Self {
Self {
seq_count: Cell::new(0),
max_val: $ty::MAX
}
}
}
impl Default for SeqCountProviderSimple<$ty> {
fn default() -> Self {
Self::[<new_ $ty>]()
}
}
impl SequenceCountProvider for SeqCountProviderSimple<$ty> {
type Raw = $ty;
const MAX_BIT_WIDTH: usize = core::mem::size_of::<Self::Raw>() * 8;
fn get(&self) -> Self::Raw {
self.seq_count.get()
}
fn increment(&self) {
self.get_and_increment();
}
fn get_and_increment(&self) -> Self::Raw {
let curr_count = self.seq_count.get();
if curr_count == self.max_val {
self.seq_count.set(0);
} else {
self.seq_count.set(curr_count + 1);
}
curr_count
}
}
}
)+
}
}
impl_for_primitives!(u8, u16, u32, u64,);
/// This is a sequence count provider which wraps around at [MAX_SEQ_COUNT].
#[derive(Clone)]
pub struct CcsdsSimpleSeqCountProvider {
provider: SeqCountProviderSimple<u16>,
}
impl Default for CcsdsSimpleSeqCountProvider {
fn default() -> Self {
Self {
provider: SeqCountProviderSimple::new_custom_max_val_u16(MAX_SEQ_COUNT),
}
}
}
impl SequenceCountProvider for CcsdsSimpleSeqCountProvider {
type Raw = u16;
const MAX_BIT_WIDTH: usize = core::mem::size_of::<Self::Raw>() * 8;
delegate::delegate! {
to self.provider {
fn get(&self) -> u16;
fn increment(&self);
fn get_and_increment(&self) -> u16;
}
}
}
#[cfg(feature = "std")]
pub mod stdmod {
use super::*;
use std::sync::{Arc, Mutex};
macro_rules! sync_clonable_seq_counter_impl {
($($ty: ident,)+) => {
$(paste! {
/// These sequence counters can be shared between threads and can also be
/// configured to wrap around at specified maximum values. Please note that
/// that the API provided by this class will not panic und [Mutex] lock errors,
/// but it will yield 0 for the getter functions.
#[derive(Clone, Default)]
pub struct [<SeqCountProviderSync $ty:upper>] {
seq_count: Arc<Mutex<$ty>>,
max_val: $ty
}
impl [<SeqCountProviderSync $ty:upper>] {
pub fn new() -> Self {
Self::new_with_max_val($ty::MAX)
}
pub fn new_with_max_val(max_val: $ty) -> Self {
Self {
seq_count: Arc::default(),
max_val
}
}
}
impl SequenceCountProvider for [<SeqCountProviderSync $ty:upper>] {
type Raw = $ty;
const MAX_BIT_WIDTH: usize = core::mem::size_of::<Self::Raw>() * 8;
fn get(&self) -> $ty {
match self.seq_count.lock() {
Ok(counter) => *counter,
Err(_) => 0
}
}
fn increment(&self) {
self.get_and_increment();
}
fn get_and_increment(&self) -> $ty {
match self.seq_count.lock() {
Ok(mut counter) => {
let val = *counter;
if val == self.max_val {
*counter = 0;
} else {
*counter += 1;
}
val
}
Err(_) => 0,
}
}
}
})+
}
}
sync_clonable_seq_counter_impl!(u8, u16, u32, u64,);
}
#[cfg(test)]
mod tests {
use crate::seq_count::{
CcsdsSimpleSeqCountProvider, SeqCountProviderSimple, SeqCountProviderSyncU8,
SequenceCountProvider,
};
use crate::MAX_SEQ_COUNT;
#[test]
fn test_u8_counter() {
let u8_counter = SeqCountProviderSimple::<u8>::default();
assert_eq!(u8_counter.get(), 0);
assert_eq!(u8_counter.get_and_increment(), 0);
assert_eq!(u8_counter.get_and_increment(), 1);
assert_eq!(u8_counter.get(), 2);
}
#[test]
fn test_u8_counter_overflow() {
let u8_counter = SeqCountProviderSimple::new_u8();
for _ in 0..256 {
u8_counter.increment();
}
assert_eq!(u8_counter.get(), 0);
}
#[test]
fn test_ccsds_counter() {
let ccsds_counter = CcsdsSimpleSeqCountProvider::default();
assert_eq!(ccsds_counter.get(), 0);
assert_eq!(ccsds_counter.get_and_increment(), 0);
assert_eq!(ccsds_counter.get_and_increment(), 1);
assert_eq!(ccsds_counter.get(), 2);
}
#[test]
fn test_ccsds_counter_overflow() {
let ccsds_counter = CcsdsSimpleSeqCountProvider::default();
for _ in 0..MAX_SEQ_COUNT + 1 {
ccsds_counter.increment();
}
assert_eq!(ccsds_counter.get(), 0);
}
#[test]
fn test_atomic_ref_counters() {
let sync_u8_counter = SeqCountProviderSyncU8::new();
assert_eq!(sync_u8_counter.get(), 0);
assert_eq!(sync_u8_counter.get_and_increment(), 0);
assert_eq!(sync_u8_counter.get_and_increment(), 1);
assert_eq!(sync_u8_counter.get(), 2);
}
#[test]
fn test_atomic_ref_counters_overflow() {
let sync_u8_counter = SeqCountProviderSyncU8::new();
for _ in 0..u8::MAX as u16 + 1 {
sync_u8_counter.increment();
}
assert_eq!(sync_u8_counter.get(), 0);
}
#[test]
fn test_atomic_ref_counters_overflow_custom_max_val() {
let sync_u8_counter = SeqCountProviderSyncU8::new_with_max_val(128);
for _ in 0..129 {
sync_u8_counter.increment();
}
assert_eq!(sync_u8_counter.get(), 0);
}
}

View File

@ -2,11 +2,8 @@
//! [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) section 3.5 .
//! See [chrono::DateTime::format] for a usage example of the generated
//! [chrono::format::DelayedFormat] structs.
#[cfg(feature = "alloc")]
use chrono::{
format::{DelayedFormat, StrftimeItems},
DateTime, Utc,
};
#[cfg(all(feature = "alloc", feature = "chrono"))]
pub use alloc_mod_chrono::*;
/// Tuple of format string and formatted size for time code A.
///
@ -34,36 +31,37 @@ pub const FMT_STR_CODE_B_WITH_SIZE: (&str, usize) = ("%Y-%jT%T%.3f", 21);
/// Three digits are used for the decimal fraction and a terminator is added at the end.
pub const FMT_STR_CODE_B_TERMINATED_WITH_SIZE: (&str, usize) = ("%Y-%jT%T%.3fZ", 22);
/// Generates a time code formatter using the [FMT_STR_CODE_A_WITH_SIZE] format.
#[cfg(feature = "alloc")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
pub fn generate_time_code_a(date: &DateTime<Utc>) -> DelayedFormat<StrftimeItems<'static>> {
date.format(FMT_STR_CODE_A_WITH_SIZE.0)
}
#[cfg(all(feature = "alloc", feature = "chrono"))]
pub mod alloc_mod_chrono {
use super::*;
use chrono::{
format::{DelayedFormat, StrftimeItems},
DateTime, Utc,
};
/// Generates a time code formatter using the [FMT_STR_CODE_A_TERMINATED_WITH_SIZE] format.
#[cfg(feature = "alloc")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
pub fn generate_time_code_a_terminated(
date: &DateTime<Utc>,
) -> DelayedFormat<StrftimeItems<'static>> {
date.format(FMT_STR_CODE_A_TERMINATED_WITH_SIZE.0)
}
/// Generates a time code formatter using the [FMT_STR_CODE_A_WITH_SIZE] format.
pub fn generate_time_code_a(date: &DateTime<Utc>) -> DelayedFormat<StrftimeItems<'static>> {
date.format(FMT_STR_CODE_A_WITH_SIZE.0)
}
/// Generates a time code formatter using the [FMT_STR_CODE_B_WITH_SIZE] format.
#[cfg(feature = "alloc")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
pub fn generate_time_code_b(date: &DateTime<Utc>) -> DelayedFormat<StrftimeItems<'static>> {
date.format(FMT_STR_CODE_B_WITH_SIZE.0)
}
/// Generates a time code formatter using the [FMT_STR_CODE_A_TERMINATED_WITH_SIZE] format.
pub fn generate_time_code_a_terminated(
date: &DateTime<Utc>,
) -> DelayedFormat<StrftimeItems<'static>> {
date.format(FMT_STR_CODE_A_TERMINATED_WITH_SIZE.0)
}
/// Generates a time code formatter using the [FMT_STR_CODE_B_TERMINATED_WITH_SIZE] format.
#[cfg(feature = "alloc")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
pub fn generate_time_code_b_terminated(
date: &DateTime<Utc>,
) -> DelayedFormat<StrftimeItems<'static>> {
date.format(FMT_STR_CODE_B_TERMINATED_WITH_SIZE.0)
/// Generates a time code formatter using the [FMT_STR_CODE_B_WITH_SIZE] format.
pub fn generate_time_code_b(date: &DateTime<Utc>) -> DelayedFormat<StrftimeItems<'static>> {
date.format(FMT_STR_CODE_B_WITH_SIZE.0)
}
/// Generates a time code formatter using the [FMT_STR_CODE_B_TERMINATED_WITH_SIZE] format.
pub fn generate_time_code_b_terminated(
date: &DateTime<Utc>,
) -> DelayedFormat<StrftimeItems<'static>> {
date.format(FMT_STR_CODE_B_TERMINATED_WITH_SIZE.0)
}
}
#[cfg(test)]
@ -73,25 +71,54 @@ mod tests {
use std::format;
#[test]
fn test_ascii_timestamp_a_unterminated() {
let date = Utc::now();
fn test_ascii_timestamp_a_unterminated_epoch() {
let date = chrono::DateTime::UNIX_EPOCH;
let stamp_formatter = generate_time_code_a(&date);
let stamp = format!("{}", stamp_formatter);
let t_sep = stamp.find("T");
let t_sep = stamp.find('T');
assert!(t_sep.is_some());
assert_eq!(t_sep.unwrap(), 10);
assert_eq!(stamp.len(), FMT_STR_CODE_A_WITH_SIZE.1);
}
#[test]
fn test_ascii_timestamp_a_terminated() {
#[cfg_attr(miri, ignore)]
fn test_ascii_timestamp_a_unterminated_now() {
let date = Utc::now();
let stamp_formatter = generate_time_code_a(&date);
let stamp = format!("{}", stamp_formatter);
let t_sep = stamp.find('T');
assert!(t_sep.is_some());
assert_eq!(t_sep.unwrap(), 10);
assert_eq!(stamp.len(), FMT_STR_CODE_A_WITH_SIZE.1);
}
#[test]
fn test_ascii_timestamp_a_terminated_epoch() {
let date = chrono::DateTime::UNIX_EPOCH;
let stamp_formatter = generate_time_code_a_terminated(&date);
let stamp = format!("{}", stamp_formatter);
let t_sep = stamp.find('T');
assert!(t_sep.is_some());
assert_eq!(t_sep.unwrap(), 10);
let z_terminator = stamp.find('Z');
assert!(z_terminator.is_some());
assert_eq!(
z_terminator.unwrap(),
FMT_STR_CODE_A_TERMINATED_WITH_SIZE.1 - 1
);
assert_eq!(stamp.len(), FMT_STR_CODE_A_TERMINATED_WITH_SIZE.1);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_ascii_timestamp_a_terminated_now() {
let date = Utc::now();
let stamp_formatter = generate_time_code_a_terminated(&date);
let stamp = format!("{}", stamp_formatter);
let t_sep = stamp.find("T");
let t_sep = stamp.find('T');
assert!(t_sep.is_some());
assert_eq!(t_sep.unwrap(), 10);
let z_terminator = stamp.find("Z");
let z_terminator = stamp.find('Z');
assert!(z_terminator.is_some());
assert_eq!(
z_terminator.unwrap(),
@ -101,25 +128,55 @@ mod tests {
}
#[test]
fn test_ascii_timestamp_b_unterminated() {
let date = Utc::now();
fn test_ascii_timestamp_b_unterminated_epoch() {
let date = chrono::DateTime::UNIX_EPOCH;
let stamp_formatter = generate_time_code_b(&date);
let stamp = format!("{}", stamp_formatter);
let t_sep = stamp.find("T");
let t_sep = stamp.find('T');
assert!(t_sep.is_some());
assert_eq!(t_sep.unwrap(), 8);
assert_eq!(stamp.len(), FMT_STR_CODE_B_WITH_SIZE.1);
}
#[test]
fn test_ascii_timestamp_b_terminated() {
#[cfg_attr(miri, ignore)]
fn test_ascii_timestamp_b_unterminated_now() {
let date = Utc::now();
let stamp_formatter = generate_time_code_b(&date);
let stamp = format!("{}", stamp_formatter);
let t_sep = stamp.find('T');
assert!(t_sep.is_some());
assert_eq!(t_sep.unwrap(), 8);
assert_eq!(stamp.len(), FMT_STR_CODE_B_WITH_SIZE.1);
}
#[test]
fn test_ascii_timestamp_b_terminated_epoch() {
let date = chrono::DateTime::UNIX_EPOCH;
let stamp_formatter = generate_time_code_b_terminated(&date);
let stamp = format!("{}", stamp_formatter);
let t_sep = stamp.find('T');
assert!(t_sep.is_some());
assert_eq!(t_sep.unwrap(), 8);
let z_terminator = stamp.find('Z');
assert!(z_terminator.is_some());
assert_eq!(
z_terminator.unwrap(),
FMT_STR_CODE_B_TERMINATED_WITH_SIZE.1 - 1
);
assert_eq!(stamp.len(), FMT_STR_CODE_B_TERMINATED_WITH_SIZE.1);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_ascii_timestamp_b_terminated_now() {
let date = Utc::now();
let stamp_formatter = generate_time_code_b_terminated(&date);
let stamp = format!("{}", stamp_formatter);
let t_sep = stamp.find("T");
let t_sep = stamp.find('T');
assert!(t_sep.is_some());
assert_eq!(t_sep.unwrap(), 8);
let z_terminator = stamp.find("Z");
let z_terminator = stamp.find('Z');
assert!(z_terminator.is_some());
assert_eq!(
z_terminator.unwrap(),

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,10 @@
//! CCSDS Time Code Formats according to [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf)
use crate::{ByteConversionError, SizeMissmatch};
use chrono::{DateTime, LocalResult, TimeZone, Utc};
use crate::ByteConversionError;
#[cfg(feature = "chrono")]
use chrono::{TimeZone, Utc};
use core::cmp::Ordering;
use core::fmt::{Display, Formatter};
use core::ops::{Add, AddAssign};
use core::ops::{Add, AddAssign, Sub};
use core::time::Duration;
#[allow(unused_imports)]
@ -12,6 +13,7 @@ use num_traits::float::FloatCore;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "std")]
use std::error::Error;
#[cfg(feature = "std")]
@ -26,10 +28,12 @@ pub mod cuc;
pub const DAYS_CCSDS_TO_UNIX: i32 = -4383;
pub const SECONDS_PER_DAY: u32 = 86400;
pub const MS_PER_DAY: u32 = SECONDS_PER_DAY * 1000;
pub const NANOS_PER_SECOND: u32 = 1_000_000_000;
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum CcsdsTimeCodes {
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum CcsdsTimeCode {
CucCcsdsEpoch = 0b001,
CucAgencyEpoch = 0b010,
Cds = 0b100,
@ -37,16 +41,16 @@ pub enum CcsdsTimeCodes {
AgencyDefined = 0b110,
}
impl TryFrom<u8> for CcsdsTimeCodes {
impl TryFrom<u8> for CcsdsTimeCode {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
x if x == CcsdsTimeCodes::CucCcsdsEpoch as u8 => Ok(CcsdsTimeCodes::CucCcsdsEpoch),
x if x == CcsdsTimeCodes::CucAgencyEpoch as u8 => Ok(CcsdsTimeCodes::CucAgencyEpoch),
x if x == CcsdsTimeCodes::Cds as u8 => Ok(CcsdsTimeCodes::Cds),
x if x == CcsdsTimeCodes::Ccs as u8 => Ok(CcsdsTimeCodes::Ccs),
x if x == CcsdsTimeCodes::AgencyDefined as u8 => Ok(CcsdsTimeCodes::AgencyDefined),
x if x == CcsdsTimeCode::CucCcsdsEpoch as u8 => Ok(CcsdsTimeCode::CucCcsdsEpoch),
x if x == CcsdsTimeCode::CucAgencyEpoch as u8 => Ok(CcsdsTimeCode::CucAgencyEpoch),
x if x == CcsdsTimeCode::Cds as u8 => Ok(CcsdsTimeCode::Cds),
x if x == CcsdsTimeCode::Ccs as u8 => Ok(CcsdsTimeCode::Ccs),
x if x == CcsdsTimeCode::AgencyDefined as u8 => Ok(CcsdsTimeCode::AgencyDefined),
_ => Err(()),
}
}
@ -54,45 +58,46 @@ impl TryFrom<u8> for CcsdsTimeCodes {
/// Retrieve the CCSDS time code from the p-field. If no valid time code identifier is found, the
/// value of the raw time code identification field is returned.
pub fn ccsds_time_code_from_p_field(pfield: u8) -> Result<CcsdsTimeCodes, u8> {
pub fn ccsds_time_code_from_p_field(pfield: u8) -> Result<CcsdsTimeCode, u8> {
let raw_bits = (pfield >> 4) & 0b111;
CcsdsTimeCodes::try_from(raw_bits).map_err(|_| raw_bits)
CcsdsTimeCode::try_from(raw_bits).map_err(|_| raw_bits)
}
#[derive(Debug, PartialEq, Eq, Copy, Clone, thiserror::Error)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[error("date before ccsds epoch: {0:?}")]
pub struct DateBeforeCcsdsEpochError(UnixTime);
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub enum TimestampError {
/// Contains tuple where first value is the expected time code and the second
/// value is the found raw value
InvalidTimeCode(CcsdsTimeCodes, u8),
InvalidTimeCode { expected: CcsdsTimeCode, found: u8 },
ByteConversion(ByteConversionError),
Cds(cds::CdsError),
Cuc(cuc::CucError),
DateBeforeCcsdsEpoch(DateTime<Utc>),
CustomEpochNotSupported,
}
impl Display for TimestampError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
TimestampError::InvalidTimeCode(time_code, raw_val) => {
TimestampError::InvalidTimeCode { expected, found } => {
write!(
f,
"invalid raw time code value {raw_val} for time code {time_code:?}"
"invalid raw time code value {found} for time code {expected:?}"
)
}
TimestampError::Cds(e) => {
write!(f, "cds error {e}")
write!(f, "cds error: {e}")
}
TimestampError::Cuc(e) => {
write!(f, "cuc error {e}")
write!(f, "cuc error: {e}")
}
TimestampError::ByteConversion(e) => {
write!(f, "byte conversion error {e}")
}
TimestampError::DateBeforeCcsdsEpoch(e) => {
write!(f, "datetime with date before ccsds epoch: {e}")
write!(f, "time stamp: {e}")
}
TimestampError::CustomEpochNotSupported => {
write!(f, "custom epochs are not supported")
@ -112,6 +117,7 @@ impl Error for TimestampError {
}
}
}
impl From<cds::CdsError> for TimestampError {
fn from(e: cds::CdsError) -> Self {
TimestampError::Cds(e)
@ -125,7 +131,6 @@ impl From<cuc::CucError> for TimestampError {
}
#[cfg(feature = "std")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))]
pub mod std_mod {
use crate::time::TimestampError;
use std::time::SystemTimeError;
@ -133,7 +138,7 @@ pub mod std_mod {
#[derive(Debug, Clone, Error)]
pub enum StdTimestampError {
#[error("system time error: {0}")]
#[error("system time error: {0:?}")]
SystemTime(#[from] SystemTimeError),
#[error("timestamp error: {0}")]
Timestamp(#[from] TimestampError),
@ -141,7 +146,6 @@ pub mod std_mod {
}
#[cfg(feature = "std")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))]
pub fn seconds_since_epoch() -> f64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
@ -153,6 +157,7 @@ pub fn seconds_since_epoch() -> f64 {
///
/// - CCSDS epoch: 1958-01-01T00:00:00+00:00
/// - UNIX Epoch: 1970-01-01T00:00:00+00:00
#[inline]
pub const fn unix_to_ccsds_days(unix_days: i64) -> i64 {
unix_days - DAYS_CCSDS_TO_UNIX as i64
}
@ -161,22 +166,24 @@ pub const fn unix_to_ccsds_days(unix_days: i64) -> i64 {
///
/// - CCSDS epoch: 1958-01-01T00:00:00+00:00
/// - UNIX Epoch: 1970-01-01T00:00:00+00:00
#[inline]
pub const fn ccsds_to_unix_days(ccsds_days: i64) -> i64 {
ccsds_days + DAYS_CCSDS_TO_UNIX as i64
}
/// Similar to [unix_to_ccsds_days] but converts the epoch instead, which is the number of elpased
/// seconds since the CCSDS and UNIX epoch times.
#[inline]
pub const fn unix_epoch_to_ccsds_epoch(unix_epoch: i64) -> i64 {
unix_epoch - (DAYS_CCSDS_TO_UNIX as i64 * SECONDS_PER_DAY as i64)
}
#[inline]
pub const fn ccsds_epoch_to_unix_epoch(ccsds_epoch: i64) -> i64 {
ccsds_epoch + (DAYS_CCSDS_TO_UNIX as i64 * SECONDS_PER_DAY as i64)
}
#[cfg(feature = "std")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))]
pub fn ms_of_day_using_sysclock() -> u32 {
ms_of_day(seconds_since_epoch())
}
@ -190,21 +197,29 @@ pub fn ms_of_day(seconds_since_epoch: f64) -> u32 {
}
pub trait TimeWriter {
fn len_written(&self) -> usize;
/// Generic function to convert write a timestamp into a raw buffer.
/// Returns the number of written bytes on success.
fn write_to_bytes(&self, bytes: &mut [u8]) -> Result<usize, TimestampError>;
#[cfg(feature = "alloc")]
fn to_vec(&self) -> Result<alloc::vec::Vec<u8>, TimestampError> {
let mut vec = alloc::vec![0; self.len_written()];
self.write_to_bytes(&mut vec)?;
Ok(vec)
}
}
pub trait TimeReader {
fn from_bytes(buf: &[u8]) -> Result<Self, TimestampError>
where
Self: Sized;
pub trait TimeReader: Sized {
fn from_bytes(buf: &[u8]) -> Result<Self, TimestampError>;
}
/// Trait for generic CCSDS time providers.
///
/// The UNIX helper methods and the [Self::date_time] method are not strictly necessary but extremely
/// The UNIX helper methods and the helper method are not strictly necessary but extremely
/// practical because they are a very common and simple exchange format for time information.
/// Therefore, it was decided to keep them in this trait as well.
pub trait CcsdsTimeProvider {
fn len_as_bytes(&self) -> usize;
@ -213,148 +228,244 @@ pub trait CcsdsTimeProvider {
/// entry denotes the length of the pfield and the second entry is the value of the pfield
/// in big endian format.
fn p_field(&self) -> (usize, [u8; 2]);
fn ccdsd_time_code(&self) -> CcsdsTimeCodes;
fn ccdsd_time_code(&self) -> CcsdsTimeCode;
fn unix_seconds(&self) -> i64;
fn subsecond_millis(&self) -> Option<u16>;
fn unix_stamp(&self) -> UnixTimestamp {
if self.subsecond_millis().is_none() {
return UnixTimestamp::new_only_seconds(self.unix_seconds());
}
UnixTimestamp::const_new(self.unix_seconds(), self.subsecond_millis().unwrap())
fn unix_secs(&self) -> i64;
fn subsec_nanos(&self) -> u32;
fn subsec_millis(&self) -> u16 {
(self.subsec_nanos() / 1_000_000) as u16
}
fn date_time(&self) -> Option<DateTime<Utc>>;
fn unix_time(&self) -> UnixTime {
UnixTime::new(self.unix_secs(), self.subsec_nanos())
}
#[cfg(feature = "chrono")]
fn chrono_date_time(&self) -> chrono::LocalResult<chrono::DateTime<chrono::Utc>> {
chrono::Utc.timestamp_opt(self.unix_secs(), self.subsec_nanos())
}
#[cfg(feature = "timelib")]
fn timelib_date_time(&self) -> Result<time::OffsetDateTime, time::error::ComponentRange> {
Ok(time::OffsetDateTime::from_unix_timestamp(self.unix_secs())?
+ time::Duration::nanoseconds(self.subsec_nanos().into()))
}
}
/// UNIX timestamp: Elapsed seconds since 1970-01-01T00:00:00+00:00.
/// UNIX time: Elapsed non-leap seconds since 1970-01-01T00:00:00+00:00 UTC.
///
/// Also can optionally include subsecond millisecond for greater accuracy. Please note that a
/// subsecond millisecond value of 0 gets converted to [None].
/// This is a commonly used time format and can therefore also be used as a generic format to
/// convert other CCSDS time formats to and from. The subsecond precision is in nanoseconds
/// similarly to other common time formats and libraries.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct UnixTimestamp {
pub unix_seconds: i64,
subsecond_millis: Option<u16>,
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct UnixTime {
secs: i64,
subsec_nanos: u32,
}
impl UnixTimestamp {
/// Returns none if the subsecond millisecond value is larger than 999. 0 is converted to
/// a [None] value.
pub fn new(unix_seconds: i64, subsec_millis: u16) -> Option<Self> {
if subsec_millis > 999 {
impl UnixTime {
/// The UNIX epoch time: 1970-01-01T00:00:00+00:00 UTC.
pub const EPOCH: Self = Self {
secs: 0,
subsec_nanos: 0,
};
/// The minimum possible `UnixTime`.
pub const MIN: Self = Self {
secs: i64::MIN,
subsec_nanos: 0,
};
/// The maximum possible `UnixTime`.
pub const MAX: Self = Self {
secs: i64::MAX,
subsec_nanos: NANOS_PER_SECOND - 1,
};
/// Returns [None] if the subsecond nanosecond value is invalid (larger than fraction of a
/// second)
pub fn new_checked(unix_seconds: i64, subsec_nanos: u32) -> Option<Self> {
if subsec_nanos >= NANOS_PER_SECOND {
return None;
}
Some(Self::const_new(unix_seconds, subsec_millis))
Some(Self::new(unix_seconds, subsec_nanos))
}
/// Like [Self::new] but const. Panics if the subsecond value is larger than 999.
pub const fn const_new(unix_seconds: i64, subsec_millis: u16) -> Self {
if subsec_millis > 999 {
panic!("subsec milliseconds exceeds 999");
/// Returns [None] if the subsecond millisecond value is invalid (larger than fraction of a
/// second)
pub fn new_subsec_millis_checked(unix_seconds: i64, subsec_millis: u16) -> Option<Self> {
if subsec_millis >= 1000 {
return None;
}
Self::new_checked(unix_seconds, subsec_millis as u32 * 1_000_000)
}
/// This function will panic if the subsecond value is larger than the fraction of a second.
/// Use [Self::new_checked] if you want to handle this case without a panic.
pub const fn new(unix_seconds: i64, subsecond_nanos: u32) -> Self {
if subsecond_nanos >= NANOS_PER_SECOND {
panic!("invalid subsecond nanos value");
}
let subsecond_millis = if subsec_millis == 0 {
None
} else {
Some(subsec_millis)
};
Self {
unix_seconds,
subsecond_millis,
secs: unix_seconds,
subsec_nanos: subsecond_nanos,
}
}
pub fn new_only_seconds(unix_seconds: i64) -> Self {
/// This function will panic if the subsecond value is larger than the fraction of a second.
/// Use [Self::new_subsec_millis_checked] if you want to handle this case without a panic.
pub const fn new_subsec_millis(unix_seconds: i64, subsecond_millis: u16) -> Self {
if subsecond_millis >= 1000 {
panic!("invalid subsecond millisecond value");
}
Self {
unix_seconds,
subsecond_millis: None,
secs: unix_seconds,
subsec_nanos: subsecond_millis as u32 * 1_000_000,
}
}
pub fn subsecond_millis(&self) -> Option<u16> {
self.subsecond_millis
}
#[cfg(feature = "std")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "std")))]
pub fn from_now() -> Result<Self, SystemTimeError> {
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
let epoch = now.as_secs();
Ok(Self::const_new(epoch as i64, now.subsec_millis() as u16))
pub fn new_only_secs(unix_seconds: i64) -> Self {
Self {
secs: unix_seconds,
subsec_nanos: 0,
}
}
#[inline]
pub fn unix_seconds_f64(&self) -> f64 {
let mut secs = self.unix_seconds as f64;
if let Some(subsec_millis) = self.subsecond_millis {
secs += subsec_millis as f64 / 1000.0;
}
secs
pub fn subsec_millis(&self) -> u16 {
(self.subsec_nanos / 1_000_000) as u16
}
pub fn as_date_time(&self) -> LocalResult<DateTime<Utc>> {
Utc.timestamp_opt(
self.unix_seconds,
self.subsecond_millis.unwrap_or(0) as u32 * 10_u32.pow(6),
)
pub fn subsec_nanos(&self) -> u32 {
self.subsec_nanos
}
#[cfg(feature = "std")]
pub fn now() -> Result<Self, SystemTimeError> {
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
let epoch = now.as_secs();
Ok(Self::new(epoch as i64, now.subsec_nanos()))
}
#[inline]
pub fn unix_secs_f64(&self) -> f64 {
self.secs as f64 + (self.subsec_nanos as f64 / 1_000_000_000.0)
}
pub fn as_secs(&self) -> i64 {
self.secs
}
#[cfg(feature = "chrono")]
pub fn chrono_date_time(&self) -> chrono::LocalResult<chrono::DateTime<chrono::Utc>> {
Utc.timestamp_opt(self.secs, self.subsec_nanos)
}
#[cfg(feature = "timelib")]
pub fn timelib_date_time(&self) -> Result<time::OffsetDateTime, time::error::ComponentRange> {
Ok(time::OffsetDateTime::from_unix_timestamp(self.as_secs())?
+ time::Duration::nanoseconds(self.subsec_nanos().into()))
}
// Calculate the difference in milliseconds between two UnixTimestamps
pub fn diff_in_millis(&self, other: &UnixTime) -> Option<i64> {
let seconds_difference = self.secs.checked_sub(other.secs)?;
// Convert seconds difference to milliseconds
let milliseconds_difference = seconds_difference.checked_mul(1000)?;
// Calculate the difference in subsecond milliseconds directly
let subsecond_difference_nanos = self.subsec_nanos as i64 - other.subsec_nanos as i64;
// Combine the differences
Some(milliseconds_difference + (subsecond_difference_nanos / 1_000_000))
}
}
impl From<DateTime<Utc>> for UnixTimestamp {
fn from(value: DateTime<Utc>) -> Self {
Self::const_new(value.timestamp(), value.timestamp_subsec_millis() as u16)
#[cfg(feature = "chrono")]
impl From<chrono::DateTime<chrono::Utc>> for UnixTime {
fn from(value: chrono::DateTime<chrono::Utc>) -> Self {
Self::new(value.timestamp(), value.timestamp_subsec_nanos())
}
}
impl PartialOrd for UnixTimestamp {
#[cfg(feature = "timelib")]
impl From<time::OffsetDateTime> for UnixTime {
fn from(value: time::OffsetDateTime) -> Self {
Self::new(value.unix_timestamp(), value.nanosecond())
}
}
impl PartialOrd for UnixTime {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for UnixTime {
fn cmp(&self, other: &Self) -> Ordering {
if self == other {
return Some(Ordering::Equal);
return Ordering::Equal;
}
match self.unix_seconds.cmp(&other.unix_seconds) {
Ordering::Less => return Some(Ordering::Less),
Ordering::Greater => return Some(Ordering::Greater),
match self.secs.cmp(&other.secs) {
Ordering::Less => return Ordering::Less,
Ordering::Greater => return Ordering::Greater,
_ => (),
}
match self
.subsecond_millis()
.unwrap_or(0)
.cmp(&other.subsecond_millis().unwrap_or(0))
{
match self.subsec_millis().cmp(&other.subsec_millis()) {
Ordering::Less => {
return if self.unix_seconds < 0 {
Some(Ordering::Greater)
return if self.secs < 0 {
Ordering::Greater
} else {
Some(Ordering::Less)
Ordering::Less
}
}
Ordering::Greater => {
return if self.unix_seconds < 0 {
Some(Ordering::Less)
return if self.secs < 0 {
Ordering::Less
} else {
Some(Ordering::Greater)
Ordering::Greater
}
}
Ordering::Equal => (),
}
Some(Ordering::Equal)
Ordering::Equal
}
}
impl Ord for UnixTimestamp {
fn cmp(&self, other: &Self) -> Ordering {
PartialOrd::partial_cmp(self, other).unwrap()
/// Difference between two UNIX timestamps. The [Duration] type can not contain negative durations,
/// so the sign information is supplied separately.
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct StampDiff {
pub positive_duration: bool,
pub duration_absolute: Duration,
}
impl Sub for UnixTime {
type Output = Option<StampDiff>;
fn sub(self, rhs: Self) -> Self::Output {
let difference = self.diff_in_millis(&rhs)?;
Some(if difference < 0 {
StampDiff {
positive_duration: false,
duration_absolute: Duration::from_millis(-difference as u64),
}
} else {
StampDiff {
positive_duration: true,
duration_absolute: Duration::from_millis(difference as u64),
}
})
}
}
fn get_new_stamp_after_addition(
current_stamp: &UnixTimestamp,
duration: Duration,
) -> UnixTimestamp {
let mut new_subsec_millis =
current_stamp.subsecond_millis().unwrap_or(0) + duration.subsec_millis() as u16;
let mut new_unix_seconds = current_stamp.unix_seconds;
fn get_new_stamp_after_addition(current_stamp: &UnixTime, duration: Duration) -> UnixTime {
let mut new_subsec_nanos = current_stamp.subsec_nanos() + duration.subsec_nanos();
let mut new_unix_seconds = current_stamp.secs;
let mut increment_seconds = |value: u32| {
if new_unix_seconds < 0 {
new_unix_seconds = new_unix_seconds
@ -366,8 +477,8 @@ fn get_new_stamp_after_addition(
.expect("new unix seconds would exceed i64::MAX");
}
};
if new_subsec_millis >= 1000 {
new_subsec_millis -= 1000;
if new_subsec_nanos >= 1_000_000_000 {
new_subsec_nanos -= 1_000_000_000;
increment_seconds(1);
}
increment_seconds(
@ -376,7 +487,7 @@ fn get_new_stamp_after_addition(
.try_into()
.expect("duration seconds exceeds u32::MAX"),
);
UnixTimestamp::const_new(new_unix_seconds, new_subsec_millis)
UnixTime::new(new_unix_seconds, new_subsec_nanos)
}
/// Please note that this operation will panic on the following conditions:
@ -384,7 +495,7 @@ fn get_new_stamp_after_addition(
/// - Unix seconds after subtraction for stamps before the unix epoch exceeds [i64::MIN].
/// - Unix seconds after addition exceeds [i64::MAX].
/// - Seconds from duration to add exceeds [u32::MAX].
impl AddAssign<Duration> for UnixTimestamp {
impl AddAssign<Duration> for UnixTime {
fn add_assign(&mut self, duration: Duration) {
*self = get_new_stamp_after_addition(self, duration);
}
@ -395,7 +506,7 @@ impl AddAssign<Duration> for UnixTimestamp {
/// - Unix seconds after subtraction for stamps before the unix epoch exceeds [i64::MIN].
/// - Unix seconds after addition exceeds [i64::MAX].
/// - Unix seconds exceeds [u32::MAX].
impl Add<Duration> for UnixTimestamp {
impl Add<Duration> for UnixTime {
type Output = Self;
fn add(self, duration: Duration) -> Self::Output {
@ -403,8 +514,8 @@ impl Add<Duration> for UnixTimestamp {
}
}
impl Add<Duration> for &UnixTimestamp {
type Output = UnixTimestamp;
impl Add<Duration> for &UnixTime {
type Output = UnixTime;
fn add(self, duration: Duration) -> Self::Output {
get_new_stamp_after_addition(self, duration)
@ -413,7 +524,16 @@ impl Add<Duration> for &UnixTimestamp {
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use alloc::string::ToString;
use chrono::{Datelike, Timelike};
use std::{format, println};
use super::{cuc::CucError, *};
#[allow(dead_code)]
const UNIX_STAMP_CONST: UnixTime = UnixTime::new(5, 999_999_999);
#[allow(dead_code)]
const UNIX_STAMP_CONST_2: UnixTime = UnixTime::new_subsec_millis(5, 999);
#[test]
fn test_days_conversion() {
@ -422,12 +542,22 @@ mod tests {
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_get_current_time() {
let sec_floats = seconds_since_epoch();
assert!(sec_floats > 0.0);
}
#[test]
fn test_ms_of_day() {
let ms = ms_of_day(0.0);
assert_eq!(ms, 0);
let ms = ms_of_day(5.0);
assert_eq!(ms, 5000);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_ccsds_epoch() {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
@ -442,29 +572,29 @@ mod tests {
#[test]
fn basic_unix_stamp_test() {
let stamp = UnixTimestamp::new_only_seconds(-200);
assert_eq!(stamp.unix_seconds, -200);
assert!(stamp.subsecond_millis().is_none());
let stamp = UnixTimestamp::new_only_seconds(250);
assert_eq!(stamp.unix_seconds, 250);
assert!(stamp.subsecond_millis().is_none());
let stamp = UnixTime::new_only_secs(-200);
assert_eq!(stamp.secs, -200);
assert_eq!(stamp.subsec_millis(), 0);
let stamp = UnixTime::new_only_secs(250);
assert_eq!(stamp.secs, 250);
assert_eq!(stamp.subsec_millis(), 0);
}
#[test]
fn basic_float_unix_stamp_test() {
let stamp = UnixTimestamp::new(500, 600).unwrap();
assert!(stamp.subsecond_millis.is_some());
assert_eq!(stamp.unix_seconds, 500);
let subsec_millis = stamp.subsecond_millis().unwrap();
let stamp = UnixTime::new_subsec_millis_checked(500, 600).unwrap();
assert_eq!(stamp.secs, 500);
let subsec_millis = stamp.subsec_millis();
assert_eq!(subsec_millis, 600);
assert!((500.6 - stamp.unix_seconds_f64()).abs() < 0.0001);
println!("{:?}", (500.6 - stamp.unix_secs_f64()).to_string());
assert!((500.6 - stamp.unix_secs_f64()).abs() < 0.0001);
}
#[test]
fn test_ord_larger() {
let stamp0 = UnixTimestamp::new_only_seconds(5);
let stamp1 = UnixTimestamp::new(5, 500).unwrap();
let stamp2 = UnixTimestamp::new_only_seconds(6);
let stamp0 = UnixTime::new_only_secs(5);
let stamp1 = UnixTime::new_subsec_millis_checked(5, 500).unwrap();
let stamp2 = UnixTime::new_only_secs(6);
assert!(stamp1 > stamp0);
assert!(stamp2 > stamp0);
assert!(stamp2 > stamp1);
@ -472,9 +602,9 @@ mod tests {
#[test]
fn test_ord_smaller() {
let stamp0 = UnixTimestamp::new_only_seconds(5);
let stamp1 = UnixTimestamp::new(5, 500).unwrap();
let stamp2 = UnixTimestamp::new_only_seconds(6);
let stamp0 = UnixTime::new_only_secs(5);
let stamp1 = UnixTime::new_subsec_millis_checked(5, 500).unwrap();
let stamp2 = UnixTime::new_only_secs(6);
assert!(stamp0 < stamp1);
assert!(stamp0 < stamp2);
assert!(stamp1 < stamp2);
@ -482,9 +612,9 @@ mod tests {
#[test]
fn test_ord_larger_neg_numbers() {
let stamp0 = UnixTimestamp::new_only_seconds(-5);
let stamp1 = UnixTimestamp::new(-5, 500).unwrap();
let stamp2 = UnixTimestamp::new_only_seconds(-6);
let stamp0 = UnixTime::new_only_secs(-5);
let stamp1 = UnixTime::new_subsec_millis_checked(-5, 500).unwrap();
let stamp2 = UnixTime::new_only_secs(-6);
assert!(stamp0 > stamp1);
assert!(stamp0 > stamp2);
assert!(stamp1 > stamp2);
@ -494,9 +624,9 @@ mod tests {
#[test]
fn test_ord_smaller_neg_numbers() {
let stamp0 = UnixTimestamp::new_only_seconds(-5);
let stamp1 = UnixTimestamp::new(-5, 500).unwrap();
let stamp2 = UnixTimestamp::new_only_seconds(-6);
let stamp0 = UnixTime::new_only_secs(-5);
let stamp1 = UnixTime::new_subsec_millis_checked(-5, 500).unwrap();
let stamp2 = UnixTime::new_only_secs(-6);
assert!(stamp2 < stamp1);
assert!(stamp2 < stamp0);
assert!(stamp1 < stamp0);
@ -504,10 +634,11 @@ mod tests {
assert!(stamp2 <= stamp1);
}
#[allow(clippy::nonminimal_bool)]
#[test]
fn test_eq() {
let stamp0 = UnixTimestamp::new(5, 0).unwrap();
let stamp1 = UnixTimestamp::new_only_seconds(5);
let stamp0 = UnixTime::new(5, 0);
let stamp1 = UnixTime::new_only_secs(5);
assert_eq!(stamp0, stamp1);
assert!(stamp0 <= stamp1);
assert!(stamp0 >= stamp1);
@ -517,32 +648,115 @@ mod tests {
#[test]
fn test_addition() {
let mut stamp0 = UnixTimestamp::new_only_seconds(1);
let mut stamp0 = UnixTime::new_only_secs(1);
stamp0 += Duration::from_secs(5);
assert_eq!(stamp0.unix_seconds, 6);
assert!(stamp0.subsecond_millis().is_none());
assert_eq!(stamp0.as_secs(), 6);
assert_eq!(stamp0.subsec_millis(), 0);
let stamp1 = stamp0 + Duration::from_millis(500);
assert_eq!(stamp1.unix_seconds, 6);
assert!(stamp1.subsecond_millis().is_some());
assert_eq!(stamp1.subsecond_millis().unwrap(), 500);
assert_eq!(stamp1.secs, 6);
assert_eq!(stamp1.subsec_millis(), 500);
}
#[test]
fn test_addition_on_ref() {
let stamp0 = &UnixTimestamp::new(20, 500).unwrap();
let stamp0 = &UnixTime::new_subsec_millis_checked(20, 500).unwrap();
let stamp1 = stamp0 + Duration::from_millis(2500);
assert_eq!(stamp1.unix_seconds, 23);
assert!(stamp1.subsecond_millis().is_none());
assert_eq!(stamp1.secs, 23);
assert_eq!(stamp1.subsec_millis(), 0);
}
#[test]
fn test_as_dt() {
let stamp = UnixTime::new_only_secs(0);
let dt = stamp.chrono_date_time().unwrap();
assert_eq!(dt.year(), 1970);
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 1);
assert_eq!(dt.hour(), 0);
assert_eq!(dt.minute(), 0);
assert_eq!(dt.second(), 0);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_from_now() {
let stamp_now = UnixTime::now().unwrap();
let dt_now = stamp_now.chrono_date_time().unwrap();
assert!(dt_now.year() >= 2020);
}
#[test]
fn test_stamp_diff_positive_0() {
let stamp_later = UnixTime::new(2, 0);
let StampDiff {
positive_duration,
duration_absolute,
} = (stamp_later - UnixTime::new(1, 0)).expect("stamp diff error");
assert!(positive_duration);
assert_eq!(duration_absolute, Duration::from_secs(1));
}
#[test]
fn test_stamp_diff_positive_1() {
let stamp_later = UnixTime::new(3, 800 * 1_000_000);
let stamp_earlier = UnixTime::new_subsec_millis_checked(1, 900).unwrap();
let StampDiff {
positive_duration,
duration_absolute,
} = (stamp_later - stamp_earlier).expect("stamp diff error");
assert!(positive_duration);
assert_eq!(duration_absolute, Duration::from_millis(1900));
}
#[test]
fn test_stamp_diff_negative() {
let stamp_later = UnixTime::new_subsec_millis_checked(3, 800).unwrap();
let stamp_earlier = UnixTime::new_subsec_millis_checked(1, 900).unwrap();
let StampDiff {
positive_duration,
duration_absolute,
} = (stamp_earlier - stamp_later).expect("stamp diff error");
assert!(!positive_duration);
assert_eq!(duration_absolute, Duration::from_millis(1900));
}
#[test]
fn test_addition_spillover() {
let mut stamp0 = UnixTimestamp::new(1, 900).unwrap();
let mut stamp0 = UnixTime::new_subsec_millis_checked(1, 900).unwrap();
stamp0 += Duration::from_millis(100);
assert_eq!(stamp0.unix_seconds, 2);
assert!(stamp0.subsecond_millis().is_none());
assert_eq!(stamp0.secs, 2);
assert_eq!(stamp0.subsec_millis(), 0);
stamp0 += Duration::from_millis(1100);
assert_eq!(stamp0.unix_seconds, 3);
assert_eq!(stamp0.subsecond_millis().unwrap(), 100);
assert_eq!(stamp0.secs, 3);
assert_eq!(stamp0.subsec_millis(), 100);
}
#[test]
fn test_cuc_error_printout() {
let cuc_error = CucError::InvalidCounterWidth(12);
let stamp_error = TimestampError::from(cuc_error);
assert_eq!(stamp_error.to_string(), format!("cuc error: {cuc_error}"));
}
#[test]
#[cfg(feature = "timelib")]
fn test_unix_stamp_as_timelib_datetime() {
let stamp_epoch = UnixTime::EPOCH;
let timelib_dt = stamp_epoch.timelib_date_time().unwrap();
assert_eq!(timelib_dt.year(), 1970);
assert_eq!(timelib_dt.month(), time::Month::January);
assert_eq!(timelib_dt.day(), 1);
assert_eq!(timelib_dt.hour(), 0);
assert_eq!(timelib_dt.minute(), 0);
assert_eq!(timelib_dt.second(), 0);
}
#[test]
#[cfg(feature = "timelib")]
fn test_unix_stamp_from_timelib_datetime() {
let timelib_dt = time::OffsetDateTime::UNIX_EPOCH;
let unix_time = UnixTime::from(timelib_dt);
let timelib_converted_back = unix_time.timelib_date_time().unwrap();
assert_eq!(timelib_dt, timelib_converted_back);
}
}

View File

@ -1,4 +1,4 @@
use crate::{ByteConversionError, SizeMissmatch};
use crate::ByteConversionError;
use core::fmt::{Debug, Display, Formatter};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
@ -15,10 +15,12 @@ pub trait ToBeBytes {
impl ToBeBytes for () {
type ByteArray = [u8; 0];
#[inline]
fn written_len(&self) -> usize {
0
}
#[inline]
fn to_be_bytes(&self) -> Self::ByteArray {
[]
}
@ -27,9 +29,12 @@ impl ToBeBytes for () {
impl ToBeBytes for u8 {
type ByteArray = [u8; 1];
#[inline]
fn written_len(&self) -> usize {
1
}
#[inline]
fn to_be_bytes(&self) -> Self::ByteArray {
u8::to_be_bytes(*self)
}
@ -38,9 +43,12 @@ impl ToBeBytes for u8 {
impl ToBeBytes for u16 {
type ByteArray = [u8; 2];
#[inline]
fn written_len(&self) -> usize {
2
}
#[inline]
fn to_be_bytes(&self) -> Self::ByteArray {
u16::to_be_bytes(*self)
}
@ -49,9 +57,12 @@ impl ToBeBytes for u16 {
impl ToBeBytes for u32 {
type ByteArray = [u8; 4];
#[inline]
fn written_len(&self) -> usize {
4
}
#[inline]
fn to_be_bytes(&self) -> Self::ByteArray {
u32::to_be_bytes(*self)
}
@ -60,9 +71,12 @@ impl ToBeBytes for u32 {
impl ToBeBytes for u64 {
type ByteArray = [u8; 8];
#[inline]
fn written_len(&self) -> usize {
8
}
#[inline]
fn to_be_bytes(&self) -> Self::ByteArray {
u64::to_be_bytes(*self)
}
@ -73,6 +87,15 @@ pub trait UnsignedEnum {
fn size(&self) -> usize;
/// Write the unsigned enumeration to a raw buffer. Returns the written size on success.
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError>;
fn value(&self) -> u64;
#[cfg(feature = "alloc")]
fn to_vec(&self) -> alloc::vec::Vec<u8> {
let mut buf = alloc::vec![0; self.size()];
self.write_to_be_bytes(&mut buf).unwrap();
buf
}
}
pub trait UnsignedEnumExt: UnsignedEnum + Debug + Copy + Clone + PartialEq + Eq {}
@ -80,16 +103,22 @@ pub trait UnsignedEnumExt: UnsignedEnum + Debug + Copy + Clone + PartialEq + Eq
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum UnsignedByteFieldError {
/// Value is too large for specified width of byte field. The first value contains the width,
/// the second value contains the detected value.
ValueTooLargeForWidth((usize, u64)),
/// Value is too large for specified width of byte field.
ValueTooLargeForWidth {
width: usize,
value: u64,
},
/// Only 1, 2, 4 and 8 are allow width values. Optionally contains the expected width if
/// applicable, for example for conversions.
InvalidWidth(usize, Option<usize>),
InvalidWidth {
found: usize,
expected: Option<usize>,
},
ByteConversionError(ByteConversionError),
}
impl From<ByteConversionError> for UnsignedByteFieldError {
#[inline]
fn from(value: ByteConversionError) -> Self {
Self::ByteConversionError(value)
}
@ -101,10 +130,10 @@ impl Display for UnsignedByteFieldError {
Self::ByteConversionError(e) => {
write!(f, "low level byte conversion error: {e}")
}
Self::InvalidWidth(val, _) => {
write!(f, "invalid width {val}, only 1, 2, 4 and 8 are allowed.")
Self::InvalidWidth { found, .. } => {
write!(f, "invalid width {found}, only 1, 2, 4 and 8 are allowed.")
}
Self::ValueTooLargeForWidth((width, value)) => {
Self::ValueTooLargeForWidth { width, value } => {
write!(f, "value {value} too large for width {width}")
}
}
@ -117,26 +146,30 @@ impl Error for UnsignedByteFieldError {}
/// Type erased variant.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct UnsignedByteField {
width: usize,
value: u64,
}
impl UnsignedByteField {
pub fn new(width: usize, value: u64) -> Self {
#[inline]
pub const fn new(width: usize, value: u64) -> Self {
Self { width, value }
}
pub fn value(&self) -> u64 {
#[inline]
pub const fn value_const(&self) -> u64 {
self.value
}
#[inline]
pub fn new_from_be_bytes(width: usize, buf: &[u8]) -> Result<Self, UnsignedByteFieldError> {
if width > buf.len() {
return Err(ByteConversionError::FromSliceTooSmall(SizeMissmatch {
return Err(ByteConversionError::FromSliceTooSmall {
expected: width,
found: buf.len(),
})
}
.into());
}
match width {
@ -154,22 +187,31 @@ impl UnsignedByteField {
width,
u64::from_be_bytes(buf[0..8].try_into().unwrap()),
)),
_ => Err(UnsignedByteFieldError::InvalidWidth(width, None)),
_ => Err(UnsignedByteFieldError::InvalidWidth {
found: width,
expected: None,
}),
}
}
}
impl UnsignedEnum for UnsignedByteField {
#[inline]
fn size(&self) -> usize {
self.width
}
#[inline]
fn value(&self) -> u64 {
self.value_const()
}
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
if buf.len() < self.size() {
return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
return Err(ByteConversionError::ToSliceTooSmall {
expected: self.size(),
found: buf.len(),
}));
});
}
match self.size() {
0 => Ok(0),
@ -199,31 +241,42 @@ impl UnsignedEnum for UnsignedByteField {
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GenericUnsignedByteField<TYPE> {
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct GenericUnsignedByteField<TYPE: Copy + Into<u64>> {
value: TYPE,
}
impl<TYPE> GenericUnsignedByteField<TYPE> {
pub fn new(val: TYPE) -> Self {
impl<TYPE: Copy + Into<u64>> GenericUnsignedByteField<TYPE> {
pub const fn new(val: TYPE) -> Self {
Self { value: val }
}
pub const fn value_typed(&self) -> TYPE {
self.value
}
}
impl<TYPE: ToBeBytes> UnsignedEnum for GenericUnsignedByteField<TYPE> {
impl<TYPE: Copy + ToBeBytes + Into<u64>> UnsignedEnum for GenericUnsignedByteField<TYPE> {
#[inline]
fn size(&self) -> usize {
self.value.written_len()
}
fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
if buf.len() < self.size() {
return Err(ByteConversionError::ToSliceTooSmall(SizeMissmatch {
return Err(ByteConversionError::ToSliceTooSmall {
found: buf.len(),
expected: self.size(),
}));
});
}
buf[0..self.size()].copy_from_slice(self.value.to_be_bytes().as_ref());
buf[..self.size()].copy_from_slice(self.value.to_be_bytes().as_ref());
Ok(self.value.written_len())
}
#[inline]
fn value(&self) -> u64 {
self.value_typed().into()
}
}
pub type UnsignedByteFieldEmpty = GenericUnsignedByteField<()>;
@ -246,15 +299,20 @@ impl From<UnsignedByteFieldU8> for UnsignedByteField {
impl TryFrom<UnsignedByteField> for UnsignedByteFieldU8 {
type Error = UnsignedByteFieldError;
#[inline]
fn try_from(value: UnsignedByteField) -> Result<Self, Self::Error> {
if value.width != 1 {
return Err(UnsignedByteFieldError::InvalidWidth(value.width, Some(1)));
return Err(UnsignedByteFieldError::InvalidWidth {
found: value.width,
expected: Some(1),
});
}
Ok(Self::new(value.value as u8))
}
}
impl From<UnsignedByteFieldU16> for UnsignedByteField {
#[inline]
fn from(value: UnsignedByteFieldU16) -> Self {
Self::new(2, value.value as u64)
}
@ -263,15 +321,20 @@ impl From<UnsignedByteFieldU16> for UnsignedByteField {
impl TryFrom<UnsignedByteField> for UnsignedByteFieldU16 {
type Error = UnsignedByteFieldError;
#[inline]
fn try_from(value: UnsignedByteField) -> Result<Self, Self::Error> {
if value.width != 2 {
return Err(UnsignedByteFieldError::InvalidWidth(value.width, Some(2)));
return Err(UnsignedByteFieldError::InvalidWidth {
found: value.width,
expected: Some(2),
});
}
Ok(Self::new(value.value as u16))
}
}
impl From<UnsignedByteFieldU32> for UnsignedByteField {
#[inline]
fn from(value: UnsignedByteFieldU32) -> Self {
Self::new(4, value.value as u64)
}
@ -280,15 +343,20 @@ impl From<UnsignedByteFieldU32> for UnsignedByteField {
impl TryFrom<UnsignedByteField> for UnsignedByteFieldU32 {
type Error = UnsignedByteFieldError;
#[inline]
fn try_from(value: UnsignedByteField) -> Result<Self, Self::Error> {
if value.width != 4 {
return Err(UnsignedByteFieldError::InvalidWidth(value.width, Some(4)));
return Err(UnsignedByteFieldError::InvalidWidth {
found: value.width,
expected: Some(4),
});
}
Ok(Self::new(value.value as u32))
}
}
impl From<UnsignedByteFieldU64> for UnsignedByteField {
#[inline]
fn from(value: UnsignedByteFieldU64) -> Self {
Self::new(8, value.value)
}
@ -297,9 +365,13 @@ impl From<UnsignedByteFieldU64> for UnsignedByteField {
impl TryFrom<UnsignedByteField> for UnsignedByteFieldU64 {
type Error = UnsignedByteFieldError;
#[inline]
fn try_from(value: UnsignedByteField) -> Result<Self, Self::Error> {
if value.width != 8 {
return Err(UnsignedByteFieldError::InvalidWidth(value.width, Some(8)));
return Err(UnsignedByteFieldError::InvalidWidth {
found: value.width,
expected: Some(8),
});
}
Ok(Self::new(value.value))
}
@ -324,9 +396,11 @@ pub mod tests {
.expect("writing to raw buffer failed");
assert_eq!(len, 1);
assert_eq!(buf[0], 5);
for i in 1..8 {
assert_eq!(buf[i], 0);
for val in buf.iter().skip(1) {
assert_eq!(*val, 0);
}
assert_eq!(u8.value_typed(), 5);
assert_eq!(u8.value(), 5);
}
#[test]
@ -340,9 +414,11 @@ pub mod tests {
assert_eq!(len, 2);
let raw_val = u16::from_be_bytes(buf[0..2].try_into().unwrap());
assert_eq!(raw_val, 3823);
for i in 2..8 {
assert_eq!(buf[i], 0);
for val in buf.iter().skip(2) {
assert_eq!(*val, 0);
}
assert_eq!(u16.value_typed(), 3823);
assert_eq!(u16.value(), 3823);
}
#[test]
@ -356,9 +432,11 @@ pub mod tests {
assert_eq!(len, 4);
let raw_val = u32::from_be_bytes(buf[0..4].try_into().unwrap());
assert_eq!(raw_val, 80932);
for i in 4..8 {
(4..8).for_each(|i| {
assert_eq!(buf[i], 0);
}
});
assert_eq!(u32.value_typed(), 80932);
assert_eq!(u32.value(), 80932);
}
#[test]
@ -372,6 +450,8 @@ pub mod tests {
assert_eq!(len, 8);
let raw_val = u64::from_be_bytes(buf[0..8].try_into().unwrap());
assert_eq!(raw_val, 5999999);
assert_eq!(u64.value_typed(), 5999999);
assert_eq!(u64.value(), 5999999);
}
#[test]
@ -393,8 +473,11 @@ pub mod tests {
assert!(conv_fails.is_err());
let err = conv_fails.unwrap_err();
match err {
UnsignedByteFieldError::InvalidWidth(width, Some(expected)) => {
assert_eq!(width, 2);
UnsignedByteFieldError::InvalidWidth {
found,
expected: Some(expected),
} => {
assert_eq!(found, 2);
assert_eq!(expected, 1);
}
_ => {
@ -422,8 +505,11 @@ pub mod tests {
assert!(conv_fails.is_err());
let err = conv_fails.unwrap_err();
match err {
UnsignedByteFieldError::InvalidWidth(width, Some(expected)) => {
assert_eq!(width, 4);
UnsignedByteFieldError::InvalidWidth {
found,
expected: Some(expected),
} => {
assert_eq!(found, 4);
assert_eq!(expected, 2);
}
_ => {
@ -451,8 +537,11 @@ pub mod tests {
assert!(conv_fails.is_err());
let err = conv_fails.unwrap_err();
match err {
UnsignedByteFieldError::InvalidWidth(width, Some(expected)) => {
assert_eq!(width, 8);
UnsignedByteFieldError::InvalidWidth {
found,
expected: Some(expected),
} => {
assert_eq!(found, 8);
assert_eq!(expected, 4);
}
_ => {
@ -480,8 +569,11 @@ pub mod tests {
assert!(conv_fails.is_err());
let err = conv_fails.unwrap_err();
match err {
UnsignedByteFieldError::InvalidWidth(width, Some(expected)) => {
assert_eq!(width, 4);
UnsignedByteFieldError::InvalidWidth {
found,
expected: Some(expected),
} => {
assert_eq!(found, 4);
assert_eq!(expected, 8);
}
_ => {
@ -498,9 +590,9 @@ pub mod tests {
u8.write_to_be_bytes(&mut buf)
.expect("writing to raw buffer failed");
assert_eq!(buf[0], 5);
for i in 1..8 {
(1..8).for_each(|i| {
assert_eq!(buf[i], 0);
}
});
}
#[test]
@ -512,8 +604,8 @@ pub mod tests {
.expect("writing to raw buffer failed");
let raw_val = u16::from_be_bytes(buf[0..2].try_into().unwrap());
assert_eq!(raw_val, 3823);
for i in 2..8 {
assert_eq!(buf[i], 0);
for val in buf.iter().skip(2) {
assert_eq!(*val, 0);
}
}
@ -526,9 +618,9 @@ pub mod tests {
.expect("writing to raw buffer failed");
let raw_val = u32::from_be_bytes(buf[0..4].try_into().unwrap());
assert_eq!(raw_val, 80932);
for i in 4..8 {
(4..8).for_each(|i| {
assert_eq!(buf[i], 0);
}
});
}
#[test]
@ -582,9 +674,9 @@ pub mod tests {
assert!(res.is_err());
let err = res.unwrap_err();
match err {
ByteConversionError::ToSliceTooSmall(missmatch) => {
assert_eq!(missmatch.found, 1);
assert_eq!(missmatch.expected, 2);
ByteConversionError::ToSliceTooSmall { found, expected } => {
assert_eq!(found, 1);
assert_eq!(expected, 2);
}
_ => {
panic!("invalid exception")
@ -600,9 +692,9 @@ pub mod tests {
assert!(res.is_err());
let err = res.unwrap_err();
match err {
ByteConversionError::ToSliceTooSmall(missmatch) => {
assert_eq!(missmatch.found, 1);
assert_eq!(missmatch.expected, 2);
ByteConversionError::ToSliceTooSmall { found, expected } => {
assert_eq!(found, 1);
assert_eq!(expected, 2);
}
_ => {
panic!("invalid exception {}", err)
@ -612,11 +704,11 @@ pub mod tests {
assert!(u16.is_err());
let err = u16.unwrap_err();
if let UnsignedByteFieldError::ByteConversionError(
ByteConversionError::FromSliceTooSmall(missmatch),
ByteConversionError::FromSliceTooSmall { found, expected },
) = err
{
assert_eq!(missmatch.expected, 2);
assert_eq!(missmatch.found, 1);
assert_eq!(expected, 2);
assert_eq!(found, 1);
} else {
panic!("unexpected exception {}", err);
}
@ -630,9 +722,9 @@ pub mod tests {
assert!(res.is_err());
let err = res.unwrap_err();
match err {
ByteConversionError::ToSliceTooSmall(missmatch) => {
assert_eq!(missmatch.found, 3);
assert_eq!(missmatch.expected, 4);
ByteConversionError::ToSliceTooSmall { found, expected } => {
assert_eq!(found, 3);
assert_eq!(expected, 4);
}
_ => {
panic!("invalid exception")