Compare commits

...

110 commits

Author SHA1 Message Date
Pavel Karpy
72baada0bf client: Fix panic in SessionCreate
Do not panic if a signer was provided as a default. Also, return
`ErrMissingSigner` if no signer was provided at all.

Signed-off-by: Pavel Karpy <carpawell@morphbits.io>
2023-05-24 21:08:50 +03:00
LeL
5f07bcfec2
Drop subnet support (#420) 2023-05-24 18:47:22 +04:00
Roman Khimov
4d4c5fc70c
Revise apistatus funcs (#419)
close #417
2023-05-24 14:55:28 +03:00
Evgenii Baidakov
e7a5728d6b
client: Rename FromStatusV2 to ErrorFromV2, ToStatusV2 to ErrorToV2
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-24 13:41:58 +04:00
Roman Khimov
74cd2b12d1
Check signer type where appropriate (#418) 2023-05-23 18:46:59 +03:00
Evgenii Baidakov
26c1b26eec
client: Use error in FromStatusV2, ToStatusV2 functions
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-23 14:56:40 +04:00
Evgenii Baidakov
22ff96ad44
client: Remove status.Status type
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-23 14:49:46 +04:00
Evgenii Baidakov
277b8f7de4
client: Remove ErrFromStatus function
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-23 14:49:46 +04:00
Evgenii Baidakov
e2a88bc258
client: Remove status returning from processResponse function
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-23 14:49:36 +04:00
Evgenii Baidakov
b9ec85e5e3
status: Remove IsSuccessful and ErrToStatus functions
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-23 07:36:32 +04:00
Roman Khimov
de560d7424 *: drop subnet support
We don't need them and while there are reason to have some remnants in the
protocol itself we can remove them from the SDK immediately to not include
them into the 1.0.0 release.

Refs. nspcc-dev/neofs-api#261.

Signed-off-by: Roman Khimov <roman@nspcc.ru>
2023-05-22 22:46:41 +03:00
Roman Khimov
e99e9537a2 *: demand RFC6979 signer where appropriate
It's needed for container operations, therefore default ones for client and
pool have to be of this type as well. And it's easier to check for it before
usage.

Fixes #209.

Signed-off-by: Roman Khimov <roman@nspcc.ru>
2023-05-22 17:30:52 +03:00
Roman Khimov
2e18c3c16d client: replace client.Init() with New()
We'll need to check initialization parameters for consistency and Init doesn't
return any error, therefore it's easier to use smth.New() pattern and simplify
things a bit because uninited Client is not very useful anyway (we require
Init and there can be additional reasons for that in the future).

Refs. #164.

Signed-off-by: Roman Khimov <roman@nspcc.ru>
2023-05-22 17:03:10 +03:00
Roman Khimov
4e1390763a *: specify RFC6979 signer as mandatory where appropriate
Refs. #209.

Signed-off-by: Roman Khimov <roman@nspcc.ru>
2023-05-22 16:50:53 +03:00
Roman Khimov
7002b3b0df
Return errors instead of panic (#414)
close #390
2023-05-19 14:58:58 +03:00
Evgenii Baidakov
d1bcce5f79
client: Make MissingField error exported
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 14:45:50 +04:00
Evgenii Baidakov
4b0c67ea7a
*: Update documentation
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 14:45:50 +04:00
Evgenii Baidakov
4fe5e6022d
slicer: Replace panics with errors
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 14:45:49 +04:00
Evgenii Baidakov
5dec2b49b0
client: Replace panics with errors in sign logic
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 14:45:49 +04:00
Evgenii Baidakov
ef887b3ab1
client: Replace panics with errors in dial
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 14:45:49 +04:00
Evgenii Baidakov
f9d740487a
client: Replace panics with errors
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 14:45:48 +04:00
Evgenii Baidakov
8094342b1c
user: Add exported error for user extracting from key
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 14:30:02 +04:00
Evgenii Baidakov
cb4acec6a2
client: Remove MissingContext panic
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 14:30:01 +04:00
Roman Khimov
cfdd870755
Drop ResolveNeoFSFailures (#413)
close #406 
fixes #405
2023-05-19 12:13:24 +03:00
Evgenii Baidakov
49bc3b7202
pool: Refactor handleError to updateErrorRate
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 12:21:16 +04:00
Evgenii Baidakov
e377b3b4f6
*: Remove statusRes as unused
Fixes #405

Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 12:21:15 +04:00
Evgenii Baidakov
e0afe0807c
client: Fix error checking for go 1.18-19
Should be reverted/updated when minimum version of Go will be set to 1.20

Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 12:19:20 +04:00
Evgenii Baidakov
a4e14ab35b
client: Fix linter - unused parameter
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 12:19:19 +04:00
Evgenii Baidakov
9e1079723e
client: Fix linter - unnecessary leading newline
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 12:19:19 +04:00
Evgenii Baidakov
2f45caf8a5
client: Remove ResolveNeoFSFailures
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 12:19:19 +04:00
Evgenii Baidakov
548f911195
client: Update documentation
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 12:19:18 +04:00
Evgenii Baidakov
149b145073
client: Extend tests for apistatuses
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 12:19:18 +04:00
Evgenii Baidakov
1d952ced4e
client: Replace apistatus type check with error check
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 10:33:09 +04:00
Evgenii Baidakov
483aff30c0
client: Remove duplicate test function
TestFromStatusV2 and TestToStatusV2 have the identical testcases and code to check them

Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 10:33:09 +04:00
Evgenii Baidakov
9533a778a8
client: Move and extend status errors tests
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 10:33:08 +04:00
Evgenii Baidakov
fc0bd12101
client: Add separate error for each apistatus
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-19 10:33:08 +04:00
LeL
9d25bc7519
go.mod: update tzhash (#415) 2023-05-18 17:55:26 +04:00
Roman Khimov
d17066dfea go.mod: update tzhash
Signed-off-by: Roman Khimov <roman@nspcc.ru>
2023-05-18 16:21:39 +03:00
Roman Khimov
fec2f065e8
Set contextCall.err if missed session params (#410)
close #221
2023-05-16 12:29:28 +03:00
Evgenii Baidakov
162a15ae4b
client: Add tests for contextCall.err in session creation
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-15 07:45:14 +04:00
Evgenii Baidakov
0c7bfc2afe
client: Set contextCall.err if missed session params
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-15 07:45:06 +04:00
LeL
669c9ce9bc
.github: extend CODEOWNERS list (#411) 2023-05-12 10:14:04 +04:00
Roman Khimov
9e893fe3e9 .github: extend CODEOWNERS list
Signed-off-by: Roman Khimov <roman@nspcc.ru>
2023-05-11 18:26:42 +03:00
Roman Khimov
c97f834c6b
Accept oid.Address into client.PrmObjectGet, PrmObjectHash, PrmObjectDelete (#408) 2023-05-05 12:04:07 +03:00
Evgenii Baidakov
04ea0e8f6a
client: Accept rng object.Range into PrmObjectRange
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-05 12:17:21 +04:00
Evgenii Baidakov
626532d7dd
client: Use ByAddress instead of two calls FromContainer + ByID
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-05 12:17:12 +04:00
Evgenii Baidakov
3f603dc8eb
client: Accept oid.Address into PrmObjectHash, PrmObjectDelete, PrmObjectGet
close #404

Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-05 12:17:12 +04:00
Roman Khimov
d7a12a4846
Replace IsErrXXX functions with regular Go errors (#407)
close #220
2023-05-05 10:47:42 +03:00
Evgenii Baidakov
f34c99d538
docs: Actualize interfaces in status structs
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-05 10:17:16 +04:00
Evgenii Baidakov
ca0f19c453
client: Remove IsErrXXX functions
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-05 10:17:16 +04:00
Evgenii Baidakov
f1b438a2ac
pool: Replace IsErrXXX functions with errors
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-05 10:17:15 +04:00
Evgenii Baidakov
7d2cfff825
client: Define separate errors instead of IsErrXXX functions
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-05-05 10:17:15 +04:00
Roman Khimov
8ed98d6dec
Release 1.0.0-rc.8 (#403) 2023-04-27 14:25:13 +03:00
Roman Khimov
c0aaa66fe5 gitignore: ignore text editor backup files
Signed-off-by: Roman Khimov <roman@nspcc.ru>
2023-04-27 12:58:11 +03:00
Roman Khimov
24527b7880 CHANGELOG: add it, release 1.0.0-rc.8
Signed-off-by: Roman Khimov <roman@nspcc.ru>
2023-04-27 12:58:06 +03:00
Roman Khimov
e0d06dd444
Provide static object slicer (#382)
* closes #342
2023-04-27 12:03:40 +03:00
Leonard Lyubich
153695a03d client: Provide the ability to create slicer.Slicer instance
`CreateObject` function uses `slicer.Slicer` to read data encapsulated
behind `io.Reader` and store it in the NeoFS network. Sometimes there is
a need to init data writer (`io.Writer`)` instead of providing reader.
To cover such use-cases, it's worth to expose slicer's constructor based
on client.

Add `NewDataSlicer` constructor. Use the function in `CreateObject`.

Refs #342.

Signed-off-by: Leonard Lyubich <leonard@morphbits.io>
2023-04-26 21:40:01 +04:00
Leonard Lyubich
e2011832eb slicer: Allow to toggle homomorphic hashing
Homomorphic hashing of object payload is not always necessary. There is
a need to provide ability to skip calculation. It's also worth to not
calculate it by default since current implementation of Tillich-Zemor
algorithm has large resource cost.

Do not calculate homomorphic checksum in `Slicer` methods by default.
Provide option to enable the calculation. Make tests to randomize
calculation flag and assert results according to it.

Refs #342.

Signed-off-by: Leonard Lyubich <leonard@morphbits.io>
2023-04-26 21:40:01 +04:00
Leonard Lyubich
ab5ae28fdb slicer: Implement alternative slicing through user writing
Provide method to initialize payload stream as `io.WriteCloser`. This
approach will be useful for applications which need to control data
writing by themselves.

Refs #342.

Signed-off-by: Leonard Lyubich <leonard@morphbits.io>
2023-04-26 21:39:59 +04:00
Leonard Lyubich
0d7d03d56f client: Support static data slicing
Add `CreateObject` function which slices data stream into NeoFS objects
and saves the produced objects in the NeoFS network using `Client`.

Closes #342.

Signed-off-by: Leonard Lyubich <leonard@morphbits.io>
2023-04-26 21:38:08 +04:00
Leonard Lyubich
8eded316de Initial commit of static object slicer
There is a need to provide convenient function which allows to slice
user data into objects to be stored in NeoFS. The main challenge is to
produce objects compliant with the format described in the NeoFS
Specification.

Add `object/slicer` package. Export `Slicer` type which performs data
slicing.

Refs #342.

Signed-off-by: Leonard Lyubich <leonard@morphbits.io>
2023-04-26 21:38:05 +04:00
Roman Khimov
28a3708b4e
Replace signature package from neofs-api-go (#401)
close #219
2023-04-26 17:17:12 +03:00
Evgenii Baidakov
36b1e8442c
tests: Use dedicated function to generate signers in tests
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-26 15:03:55 +04:00
Evgenii Baidakov
25c0fd9b8e
client: Update coverage for signing
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-26 15:01:44 +04:00
Evgenii Baidakov
64c0612bdc
:* Replace ecdsa.PrivateKey with neofscrypto.Signer
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-26 14:59:21 +04:00
Evgenii Baidakov
570a628462
:* Replace signature package from neofs-api-go
close #219, #155

Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-26 14:54:12 +04:00
Evgenii Baidakov
77c2e227b9
pool: Use PublicKey like cache key instead of PrivateKey
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-25 14:08:47 +04:00
Roman Khimov
dbbb22ca28
Use CID to prevent incorrect usage in netmap (#396)
close #216
2023-04-21 15:25:53 +03:00
Evgenii Baidakov
83ca99ae92
doc: Actualize docs for ContainerNodes func
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-21 10:24:37 +04:00
Evgenii Baidakov
4b6965f209
netmap: Use CID to prevent incorrect usage in netmap
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-21 10:24:10 +04:00
Roman Khimov
16daed8140
Set external signer for requests payload (#398)
close #373
2023-04-20 14:48:09 +03:00
Evgenii Baidakov
70df422866
neofsecdsa: Add StaticSigner for externally calculated hashes
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-20 14:29:59 +04:00
Evgenii Baidakov
6ac961d41c
client: Set external signer for requests payload
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-20 14:29:58 +04:00
Roman Khimov
0bc98456e3
Remove FromString method of enumerations (#393)
close #98
2023-04-19 21:09:43 +03:00
Evgenii Baidakov
6757c0a706
linter: Fix fmt.Errorf format for type variable
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-19 16:14:22 +04:00
Evgenii Baidakov
5d3fcd6f55
enums: Remove FromString method of enumerations
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-19 14:33:35 +04:00
LeL
34213c6275
Update go version to 1.20 (#394) 2023-04-19 14:23:22 +04:00
LeL
bde3b2f4b6
Actualize CODEOWNERS (#395) 2023-04-19 14:19:40 +04:00
Roman Khimov
dcdd75b751
documentation: Container create and get example (#392)
Close #385
2023-04-18 22:49:39 +03:00
Roman Khimov
32fc84a67c .github: hey, I'm the code owner too!
Let me have this green button.

Signed-off-by: Roman Khimov <roman@nspcc.ru>
2023-04-18 21:58:12 +03:00
Evgenii Baidakov
38a613fce5
*: replace interface{} with any keyword
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-18 12:28:25 +04:00
Evgenii Baidakov
54faed8384
.github: Remove support 1.17, add support 1.20
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-18 12:21:34 +04:00
Evgenii Baidakov
c5f949e314
go.mod: Set minimum version to 1.18
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-18 12:19:34 +04:00
Evgenii Baidakov
d1ae4ef576
documentation: Container create and get example
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-17 17:39:33 +04:00
Roman Khimov
47af8441eb
[#362] pool: Don't use default session token for read (#363)
close #362
2023-04-17 15:43:06 +03:00
Roman Khimov
8678b36ed9
Check CID is set to eacl table in ContainerSetEACL command (#389)
close #384
2023-04-17 15:25:58 +03:00
Roman Khimov
c6bda422fc
filter: Add payload and homomorphic hash filter methods (#386)
close #383
2023-04-17 14:44:21 +03:00
Evgenii Baidakov
dfb799783e
client: Check CID is set to eacl table in ContainerSetEACL command
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-17 09:40:43 +04:00
Roman Khimov
493fa50915
Feature/308 add forming phy storage group (#348)
Move forming storage group (with phy objects) from
[neofs-cli](c2918fce3a/pkg/services/object_manager/storagegroup/collect.go (L17-L67))
to sdk

close #308
2023-04-15 20:53:46 +03:00
Roman Khimov
08a152b0bf
Update deps (#372) 2023-04-15 20:39:17 +03:00
Roman Khimov
3d42ae62d6
Method to take raw client (#388)
close #379
2023-04-15 14:29:38 +03:00
Evgenii Baidakov
7d66148d1f
filter: Add payload and homomorphic hash filter methods
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-14 16:37:48 +04:00
Evgenii Baidakov
77f557530e
pool: Method to take raw client
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-14 09:56:58 +04:00
Roman Khimov
bd429c9f37
object: Remove deprecated object.RawObject type (#387)
close #139
2023-04-13 23:33:37 +03:00
Evgenii Baidakov
71350c1f42
object: Remove deprecated object.RawObject type
Signed-off-by: Evgenii Baidakov <evgenii@nspcc.io>
2023-04-13 19:26:22 +04:00
Roman Khimov
9a543b6f64
Add some encoding methods (#378)
_Inspired by https://github.com/AxLabs/neofs-shared-lib project._
2023-04-13 13:00:45 +03:00
Leonard Lyubich
651c17f9b3 [#378] version: Add JSON encoding for Version type
Define `MarshalJSON` / `UnmarshalJSON` methods of the `Version` type.

Signed-off-by: Leonard Lyubich <ctulhurider@gmail.com>
2023-01-31 12:51:51 +04:00
Leonard Lyubich
8fdbf6950a [#378] netmap: Add binary encoding for NetworkInfo type
Define `Marshal` / `Unmarshal` methods of the `NetworkInfo` type.

Signed-off-by: Leonard Lyubich <ctulhurider@gmail.com>
2023-01-31 12:51:51 +04:00
Leonard Lyubich
4191e5f13e [#378] accounting: Add binary encoding for Decimal type
Define `Marshal` / `Unmarshal` methods of the `Decimal` type.

Signed-off-by: Leonard Lyubich <ctulhurider@gmail.com>
2023-01-31 12:51:47 +04:00
ae44191e8c [#374] pool: Fix handling SplitInfoError
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-01-23 11:06:41 +03:00
72df51cfe9 [#372] go.mod: Update ANTLR
Current `go:generate` command was, probably, executed only on my laptop.
Replace it with explicit version, because package in the generated code
depends on in.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2022-12-29 13:55:59 +03:00
9e18d50978 [#372] go.mod: Update dependencies
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2022-12-29 13:55:59 +03:00
5fc9865577 [#372] go.mod: Update neo-go to v0.100.1
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2022-12-29 13:55:59 +03:00
Denis Kirillov
170f31b7c4 [#365] pool: Add request callback
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-12-29 13:29:14 +03:00
Evgenii Stratonikov
9efc4ecd70 [#367] client: Allow to override key in Object.Hash RPC
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-12-29 13:28:38 +03:00
Pavel Karpy
1bf41e9bc1 [#370] bearer, session: Clarify expiration epoch
The expiration epoch is the _last_ valid epoch for an entity. Also, clarify
the expiration epoch meaning for tombstones and regular objects.

Signed-off-by: Pavel Karpy <p.karpy@yadro.com>
2022-12-29 13:28:11 +03:00
Pavel Karpy
a690dcb159 [#369] status: Make errors return default messages
Use default messages in `Error` methods like in `ToStatusV2`.

Signed-off-by: Pavel Karpy <p.karpy@yadro.com>
2022-12-12 11:36:58 +03:00
Denis Kirillov
4fbd53ba73 [#308] audit: Add CollectMembers function
Add new function to build storage group using OIDs of virtual objects.

Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-11-17 16:31:45 +03:00
Denis Kirillov
9964a83083 [#308] object/relations: List relations in split order
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-11-17 16:31:26 +03:00
Denis Kirillov
f5e1c4c31c [#362] pool: Don't use default session token for read
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-11-15 11:41:28 +03:00
137 changed files with 5092 additions and 3598 deletions

2
.github/CODEOWNERS vendored
View file

@ -1 +1 @@
* @alexvanin @fyrchik @cthulhu-rider
* @roman-khimov @cthulhu-rider @smallhive @notimetoname

View file

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
go_versions: [ '1.17.x', '1.18.x', '1.19.x' ]
go_versions: [ '1.18.x', '1.19.x', '1.20.x' ]
fail-fast: false
steps:
- uses: actions/checkout@v3

1
.gitignore vendored
View file

@ -17,6 +17,7 @@ vendor/
# IDE
.idea
.vscode
*~
# coverage
coverage.txt

60
CHANGELOG.md Normal file
View file

@ -0,0 +1,60 @@
# Changelog
## [1.0.0-rc.8] - 2023-04-27
We've reworked the way we handle keys in the SDK and added an experimental API
simplifying object creation. Pools were also improved and a number of other
minor enhancements are included in this release as well. Try it in your
applications and leave your feedback in issues, we need it to make 1.0.0 a
really good release for all NeoFS use cases.
New features:
* object/relations package for large (splitted) object data handling (#360, #348)
* pool can now accept stream operations timeout parameter (#364)
* pool can also accept callbacks to be invoked after every client response (#365)
* additional marshaling/unmarshaling methods for accounting.Decimal,
netmap.NetworkInfo and version.Version (#378)
* it's possible to get a single client from a pool now (#388)
* object.SearchFilters now provide more convenient APIs to search for object
by their hashes (#386)
* experimental client-side API for object creation (and slicing large inputs
into split objects, #382)
Behaviour changes:
* ns.ResolveContainerName changed to ns.ResolveContainerName with more
specific input type (#356)
* pool.ErrPoolClientUnhealthy is no longer exported (#358)
* deprecated object.RawObject type is gone (#387)
* pools no longer use default session tokens for read operations (they're not
required technically, #363)
* Go 1.18+ is required now (#394)
* enumeration types in eacl and object packages now have
EncodeToString/DecodeString methods replacing String/FromString for
serialization; String is still available, but it's not guaranteed to be
compatible across versions (#393)
* signing APIs no longer require a specific key, it can be provided in a
Signer wrapper (with different schemes) or a crypto.StaticSigner can be
used for signatures calculated outside of the SDK (#398, #401)
* (NetMap).PlacementVectors and (NetMap).ContainerNodes APIs now use more
specific types simplifying their use (#396)
Improvements:
* pool can work with some nodes being inaccessible now (#358)
* unified error messages in the client/status package (#369)
* documentation updates (#370, #392, #396)
* updated NeoGo, ANTLR, zap, golang-lru dependencies (#372)
* (*Client).ContainerSetEACL method now checks CID internally before
generating a request to send to the network (#389)
* neofs-api-go signature package is no longer required (#401)
Bugs fixed:
* pool.DeleteObject now correctly handles large (splitted) objects (#360)
* object structure problems are no longer considered as connection problems
by the pool (#374)
* private key used as a part of a map key in the pool (#401)
## pre-v1.0.0-rc.8
See git log.
[1.0.0-rc.8]: https://github.com/nspcc-dev/neofs-sdk-go/compare/v1.0.0-rc.7...v1.0.0-rc.8

View file

@ -42,10 +42,11 @@ Contains client for working with NeoFS.
```go
var prmInit client.PrmInit
prmInit.SetDefaultPrivateKey(key) // private key for request signing
prmInit.ResolveNeoFSFailures() // enable erroneous status parsing
var c client.Client
c.Init(prmInit)
c, err := client.New(prmInit)
if err != nil {
return
}
var prmDial client.PrmDial
prmDial.SetServerURI("grpcs://localhost:40005") // endpoint address
@ -77,8 +78,7 @@ if needed and perform any desired action. In the case above we may want to repor
these details to the user as well as retry an operation, possibly with different parameters.
Status wire-format is extendable and each node can report any set of details it wants.
The set of reserved status codes can be found in
[NeoFS API](https://github.com/nspcc-dev/neofs-api/blob/master/status/types.proto). There is also
a `client.PrmInit.ResolveNeoFSFailures()` to seamlessly convert erroneous statuses into Go error type.
[NeoFS API](https://github.com/nspcc-dev/neofs-api/blob/master/status/types.proto).
### policy
Contains helpers allowing conversion of placing policy from/to JSON representation

View file

@ -1,6 +1,8 @@
package accounting
import "github.com/nspcc-dev/neofs-api-go/v2/accounting"
import (
"github.com/nspcc-dev/neofs-api-go/v2/accounting"
)
// Decimal represents decimal number for accounting operations.
//
@ -62,3 +64,30 @@ func (d Decimal) Precision() uint32 {
func (d *Decimal) SetPrecision(p uint32) {
(*accounting.Decimal)(d).SetPrecision(p)
}
// Marshal encodes Decimal into a binary format of the NeoFS API protocol
// (Protocol Buffers with direct field order).
//
// See also Unmarshal.
func (d Decimal) Marshal() []byte {
var m accounting.Decimal
d.WriteToV2(&m)
return m.StableMarshal(nil)
}
// Unmarshal decodes NeoFS API protocol binary format into the Decimal
// (Protocol Buffers with direct field order). Returns an error describing
// a format violation.
//
// See also Marshal.
func (d *Decimal) Unmarshal(data []byte) error {
var m accounting.Decimal
err := m.Unmarshal(data)
if err != nil {
return err
}
return d.ReadFromV2(m)
}

View file

@ -5,6 +5,7 @@ import (
v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting"
"github.com/nspcc-dev/neofs-sdk-go/accounting"
accountingtest "github.com/nspcc-dev/neofs-sdk-go/accounting/test"
"github.com/stretchr/testify/require"
)
@ -44,3 +45,12 @@ func TestDecimalMessageV2(t *testing.T) {
require.EqualValues(t, d.Value(), m2.GetValue())
require.EqualValues(t, d.Precision(), m2.GetPrecision())
}
func TestDecimal_Marshal(t *testing.T) {
d := *accountingtest.Decimal()
var d2 accounting.Decimal
require.NoError(t, d2.Unmarshal(d.Marshal()))
require.Equal(t, d, d2)
}

76
audit/collect.go Normal file
View file

@ -0,0 +1,76 @@
package audit
import (
"context"
"fmt"
"github.com/nspcc-dev/neofs-sdk-go/checksum"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"github.com/nspcc-dev/neofs-sdk-go/object"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/object/relations"
"github.com/nspcc-dev/neofs-sdk-go/storagegroup"
"github.com/nspcc-dev/tzhash/tz"
)
type Collector interface {
Head(ctx context.Context, addr oid.Address) (*object.Object, error)
relations.Relations
}
// CollectMembers creates new storage group structure and fills it
// with information about members collected via HeadReceiver.
//
// Resulting storage group consists of physically stored objects only.
func CollectMembers(ctx context.Context, collector Collector, cnr cid.ID, members []oid.ID, tokens relations.Tokens, calcHomoHash bool) (*storagegroup.StorageGroup, error) {
var (
err error
sumPhySize uint64
phyMembers []oid.ID
phyHashes [][]byte
addr oid.Address
sg storagegroup.StorageGroup
)
addr.SetContainer(cnr)
for i := range members {
if phyMembers, err = relations.ListRelations(ctx, collector, cnr, members[i], tokens, false); err != nil {
return nil, err
}
for _, phyMember := range phyMembers {
addr.SetObject(phyMember)
leaf, err := collector.Head(ctx, addr)
if err != nil {
return nil, fmt.Errorf("head phy member '%s': %w", phyMember.EncodeToString(), err)
}
sumPhySize += leaf.PayloadSize()
cs, _ := leaf.PayloadHomomorphicHash()
if calcHomoHash {
phyHashes = append(phyHashes, cs.Value())
}
}
}
sg.SetMembers(phyMembers)
sg.SetValidationDataSize(sumPhySize)
if calcHomoHash {
sumHash, err := tz.Concat(phyHashes)
if err != nil {
return nil, err
}
var cs checksum.Checksum
tzHash := [64]byte{}
copy(tzHash[:], sumHash)
cs.SetTillichZemor(tzHash)
sg.SetValidationDataHash(cs)
}
return &sg, nil
}

View file

@ -1,7 +1,6 @@
package bearer
import (
"crypto/ecdsa"
"errors"
"fmt"
@ -9,7 +8,6 @@ import (
"github.com/nspcc-dev/neofs-api-go/v2/refs"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa"
"github.com/nspcc-dev/neofs-sdk-go/eacl"
"github.com/nspcc-dev/neofs-sdk-go/user"
)
@ -136,9 +134,10 @@ func (b Token) WriteToV2(m *acl.BearerToken) {
}
// SetExp sets "exp" (expiration time) claim which identifies the
// expiration time (in NeoFS epochs) on or after which the Token MUST NOT be
// accepted for processing. The processing of the "exp" claim requires that the
// current epoch MUST be before the expiration epoch listed in the "exp" claim.
// expiration time (in NeoFS epochs) after which the Token MUST NOT be
// accepted for processing. The processing of the "exp" claim requires
// that the current epoch MUST be before or equal to the expiration epoch
// listed in the "exp" claim.
//
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4.
//
@ -179,7 +178,7 @@ func (b *Token) SetIat(iat uint64) {
//
// See also SetExp, SetNbf, SetIat.
func (b Token) InvalidAt(epoch uint64) bool {
return !b.lifetimeSet || b.nbf > epoch || b.iat > epoch || b.exp <= epoch
return !b.lifetimeSet || b.nbf > epoch || b.iat > epoch || b.exp < epoch
}
// SetEACLTable sets eacl.Table that replaces the one from the issuer's
@ -244,7 +243,7 @@ func (b Token) AssertUser(id user.ID) bool {
return !b.targetUserSet || b.targetUser.Equals(id)
}
// Sign calculates and writes signature of the Token data using issuer's secret.
// Sign calculates and writes signature of the Token data using issuer's signer.
// Returns signature calculation errors.
//
// Sign MUST be called if Token is going to be transmitted over
@ -254,10 +253,10 @@ func (b Token) AssertUser(id user.ID) bool {
// expected to be calculated as a final stage of Token formation.
//
// See also VerifySignature, Issuer.
func (b *Token) Sign(key ecdsa.PrivateKey) error {
func (b *Token) Sign(signer neofscrypto.Signer) error {
var sig neofscrypto.Signature
err := sig.Calculate(neofsecdsa.Signer(key), b.signedData())
err := sig.Calculate(signer, b.signedData())
if err != nil {
return err
}
@ -359,9 +358,8 @@ func ResolveIssuer(b Token) (usr user.ID) {
binKey := b.SigningKeyBytes()
if len(binKey) != 0 {
var key neofsecdsa.PublicKey
if key.Decode(binKey) == nil {
user.IDFromKey(&usr, ecdsa.PublicKey(key))
if err := user.IDFromKey(&usr, binKey); err != nil {
usr = user.ID{}
}
}

View file

@ -5,14 +5,13 @@ import (
"math/rand"
"testing"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neofs-api-go/v2/acl"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
"github.com/nspcc-dev/neofs-sdk-go/bearer"
bearertest "github.com/nspcc-dev/neofs-sdk-go/bearer/test"
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa"
"github.com/nspcc-dev/neofs-sdk-go/crypto/test"
"github.com/nspcc-dev/neofs-sdk-go/eacl"
eacltest "github.com/nspcc-dev/neofs-sdk-go/eacl/test"
"github.com/nspcc-dev/neofs-sdk-go/user"
@ -38,7 +37,7 @@ func isEqualEACLTables(t1, t2 eacl.Table) bool {
func TestToken_SetEACLTable(t *testing.T) {
var val bearer.Token
var m acl.BearerToken
filled := bearertest.Token()
filled := bearertest.Token(t)
val.WriteToV2(&m)
require.Zero(t, m.GetBody())
@ -58,7 +57,7 @@ func TestToken_SetEACLTable(t *testing.T) {
// set value
eaclTable := *eacltest.Table()
eaclTable := *eacltest.Table(t)
val.SetEACLTable(eaclTable)
require.True(t, isEqualEACLTables(eaclTable, val.EACLTable()))
@ -84,7 +83,7 @@ func TestToken_SetEACLTable(t *testing.T) {
func TestToken_ForUser(t *testing.T) {
var val bearer.Token
var m acl.BearerToken
filled := bearertest.Token()
filled := bearertest.Token(t)
val.WriteToV2(&m)
require.Zero(t, m.GetBody())
@ -107,7 +106,7 @@ func TestToken_ForUser(t *testing.T) {
require.Zero(t, m.GetBody())
// set value
usr := *usertest.ID()
usr := *usertest.ID(t)
var usrV2 refs.OwnerID
usr.WriteToV2(&usrV2)
@ -138,7 +137,7 @@ func TestToken_ForUser(t *testing.T) {
func testLifetimeClaim(t *testing.T, setter func(*bearer.Token, uint64), getter func(*acl.BearerToken) uint64) {
var val bearer.Token
var m acl.BearerToken
filled := bearertest.Token()
filled := bearertest.Token(t)
val.WriteToV2(&m)
require.Zero(t, m.GetBody())
@ -220,7 +219,7 @@ func TestToken_InvalidAt(t *testing.T) {
require.True(t, val.InvalidAt(1))
require.False(t, val.InvalidAt(2))
require.False(t, val.InvalidAt(3))
require.True(t, val.InvalidAt(4))
require.False(t, val.InvalidAt(4))
require.True(t, val.InvalidAt(5))
}
@ -230,7 +229,7 @@ func TestToken_AssertContainer(t *testing.T) {
require.True(t, val.AssertContainer(cnr))
eaclTable := *eacltest.Table()
eaclTable := *eacltest.Table(t)
eaclTable.SetCID(cidtest.ID())
val.SetEACLTable(eaclTable)
@ -243,11 +242,11 @@ func TestToken_AssertContainer(t *testing.T) {
func TestToken_AssertUser(t *testing.T) {
var val bearer.Token
usr := *usertest.ID()
usr := *usertest.ID(t)
require.True(t, val.AssertUser(usr))
val.ForUser(*usertest.ID())
val.ForUser(*usertest.ID(t))
require.False(t, val.AssertUser(usr))
val.ForUser(usr)
@ -259,13 +258,11 @@ func TestToken_Sign(t *testing.T) {
require.False(t, val.VerifySignature())
k, err := keys.NewPrivateKey()
require.NoError(t, err)
signer := test.RandomSigner(t)
key := k.PrivateKey
val = bearertest.Token()
val = bearertest.Token(t)
require.NoError(t, val.Sign(key))
require.NoError(t, val.Sign(signer))
require.True(t, val.VerifySignature())
@ -275,7 +272,7 @@ func TestToken_Sign(t *testing.T) {
require.NotZero(t, m.GetSignature().GetKey())
require.NotZero(t, m.GetSignature().GetSign())
val2 := bearertest.Token()
val2 := bearertest.Token(t)
require.NoError(t, val2.Unmarshal(val.Marshal()))
require.True(t, val2.VerifySignature())
@ -283,7 +280,7 @@ func TestToken_Sign(t *testing.T) {
jd, err := val.MarshalJSON()
require.NoError(t, err)
val2 = bearertest.Token()
val2 = bearertest.Token(t)
require.NoError(t, val2.UnmarshalJSON(jd))
require.True(t, val2.VerifySignature())
}
@ -299,7 +296,7 @@ func TestToken_ReadFromV2(t *testing.T) {
require.Error(t, val.ReadFromV2(m))
eaclTable := eacltest.Table().ToV2()
eaclTable := eacltest.Table(t).ToV2()
body.SetEACL(eaclTable)
require.Error(t, val.ReadFromV2(m))
@ -328,7 +325,7 @@ func TestToken_ReadFromV2(t *testing.T) {
val.WriteToV2(&m2)
require.Equal(t, m, m2)
usr, usr2 := *usertest.ID(), *usertest.ID()
usr, usr2 := *usertest.ID(t), *usertest.ID(t)
require.True(t, val.AssertUser(usr))
require.True(t, val.AssertUser(usr2))
@ -346,10 +343,7 @@ func TestToken_ReadFromV2(t *testing.T) {
require.True(t, val.AssertUser(usr))
require.False(t, val.AssertUser(usr2))
k, err := keys.NewPrivateKey()
require.NoError(t, err)
signer := neofsecdsa.Signer(k.PrivateKey)
signer := test.RandomSigner(t)
var s neofscrypto.Signature
@ -363,8 +357,7 @@ func TestToken_ReadFromV2(t *testing.T) {
}
func TestResolveIssuer(t *testing.T) {
k, err := keys.NewPrivateKey()
require.NoError(t, err)
signer := test.RandomSigner(t)
var val bearer.Token
@ -381,10 +374,10 @@ func TestResolveIssuer(t *testing.T) {
require.Zero(t, bearer.ResolveIssuer(val))
require.NoError(t, val.Sign(k.PrivateKey))
require.NoError(t, val.Sign(signer))
var usr user.ID
user.IDFromKey(&usr, k.PrivateKey.PublicKey)
require.NoError(t, user.IDFromSigner(&usr, signer))
require.Equal(t, usr, bearer.ResolveIssuer(val))
}

View file

@ -1,6 +1,8 @@
package bearertest
import (
"testing"
"github.com/nspcc-dev/neofs-sdk-go/bearer"
eacltest "github.com/nspcc-dev/neofs-sdk-go/eacl/test"
usertest "github.com/nspcc-dev/neofs-sdk-go/user/test"
@ -9,12 +11,12 @@ import (
// Token returns random bearer.Token.
//
// Resulting token is unsigned.
func Token() (t bearer.Token) {
t.SetExp(3)
t.SetNbf(2)
t.SetIat(1)
t.ForUser(*usertest.ID())
t.SetEACLTable(*eacltest.Table())
func Token(t testing.TB) (tok bearer.Token) {
tok.SetExp(3)
tok.SetNbf(2)
tok.SetIat(1)
tok.ForUser(*usertest.ID(t))
tok.SetEACLTable(*eacltest.Table(t))
return t
return tok
}

View file

@ -28,8 +28,6 @@ func (x *PrmBalanceGet) SetAccount(id user.ID) {
// ResBalanceGet groups resulting values of BalanceGet operation.
type ResBalanceGet struct {
statusRes
amount accounting.Decimal
}
@ -40,23 +38,17 @@ func (x ResBalanceGet) Amount() accounting.Decimal {
// BalanceGet requests current balance of the NeoFS account.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`,
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// Any errors (local or remote, including returned status codes) are returned as Go errors,
// see [apistatus] package for NeoFS-specific error types.
//
// Immediately panics if parameters are set incorrectly (see PrmBalanceGet docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
// Return errors:
// - [ErrMissingAccount]
func (c *Client) BalanceGet(ctx context.Context, prm PrmBalanceGet) (*ResBalanceGet, error) {
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.accountSet:
panic("account not set")
return nil, ErrMissingAccount
}
// form request body
@ -81,7 +73,6 @@ func (c *Client) BalanceGet(ctx context.Context, prm PrmBalanceGet) (*ResBalance
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.Balance(&c.c, &req, client.WithContext(ctx))
}

View file

@ -7,10 +7,13 @@ import (
v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"github.com/nspcc-dev/neofs-api-go/v2/session"
)
// interface of NeoFS API server. Exists for test purposes only.
type neoFSAPIServer interface {
createSession(cli *client.Client, req *session.CreateRequest, opts ...client.CallOption) (*session.CreateResponse, error)
netMapSnapshot(context.Context, v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error)
}
@ -33,3 +36,12 @@ func (x *coreServer) netMapSnapshot(ctx context.Context, req v2netmap.SnapshotRe
return resp, nil
}
func (x *coreServer) createSession(cli *client.Client, req *session.CreateRequest, opts ...client.CallOption) (*session.CreateResponse, error) {
resp, err := rpcapi.CreateSession(cli, req, opts...)
if err != nil {
return nil, rpcErr(err)
}
return resp, nil
}

View file

@ -2,14 +2,15 @@ package client
import (
"context"
"crypto/ecdsa"
"crypto/tls"
"errors"
"fmt"
"time"
v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting"
"github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
)
// Client represents virtual connection to the NeoFS network to communicate
@ -17,28 +18,26 @@ import (
// an abstraction interface from the protocol details of data transfer over
// a network in NeoFS.
//
// Client can be created using simple Go variable declaration. Before starting
// work with the Client, it SHOULD BE correctly initialized (see Init method).
// Client can be created using [New].
// Before executing the NeoFS operations using the Client, connection to the
// server MUST BE correctly established (see Dial method and pay attention
// to the mandatory parameters). Using the Client before connecting have
// been established can lead to a panic. After the work, the Client SHOULD BE
// closed (see Close method): it frees internal and system resources which were
// allocated for the period of work of the Client. Calling Init/Dial/Close method
// allocated for the period of work of the Client. Calling [Client.Dial]/[Client.Close] method
// during the communication process step strongly discouraged as it leads to
// undefined behavior.
//
// Each method which produces a NeoFS API call may return a server response.
// Status responses are returned in the result structure, and can be cast
// to built-in error instance (or in the returned error if the client is
// configured accordingly). Certain statuses can be checked using `apistatus`
// and standard `errors` packages. Note that package provides some helper
// functions to work with status returns (e.g. IsErrContainerNotFound).
// configured accordingly). Certain statuses can be checked using [apistatus]
// and standard [errors] packages.
// All possible responses are documented in methods, however, some may be
// returned from all of them (pay attention to the presence of the pointer sign):
// - *apistatus.ServerInternal on internal server error;
// - *apistatus.NodeUnderMaintenance if a server is under maintenance;
// - *apistatus.SuccessDefaultV2 on default success.
// - *[apistatus.ServerInternal] on internal server error;
// - *[apistatus.NodeUnderMaintenance] if a server is under maintenance;
// - *[apistatus.SuccessDefaultV2] on default success.
//
// Client MUST NOT be copied by value: use pointer to Client instead.
//
@ -51,14 +50,21 @@ type Client struct {
server neoFSAPIServer
}
// Init brings the Client instance to its initial state.
var errNonNeoSigner = fmt.Errorf("%w: expected ECDSA_DETERMINISTIC_SHA256 scheme", neofscrypto.ErrIncorrectSigner)
// New creates an instance of Client initialized with the given parameters.
//
// One-time method call during application init stage (before Dial) is expected.
// Calling multiple times leads to undefined behavior.
// See docs of [PrmInit] methods for details. See also [Client.Dial]/[Client.Close].
//
// See docs of PrmInit methods for details. See also Dial / Close.
func (c *Client) Init(prm PrmInit) {
// Returned errors:
// - [neofscrypto.ErrIncorrectSigner]
func New(prm PrmInit) (*Client, error) {
var c = new(Client)
if prm.signer != nil && prm.signer.Scheme() != neofscrypto.ECDSA_DETERMINISTIC_SHA256 {
return nil, errNonNeoSigner
}
c.prm = prm
return c, nil
}
// Dial establishes a connection to the server from the NeoFS network.
@ -72,18 +78,22 @@ func (c *Client) Init(prm PrmInit) {
// Panics if required parameters are set incorrectly, look carefully
// at the method documentation.
//
// One-time method call during application start-up stage (after Init ) is expected.
// One-time method call during application start-up stage is expected.
// Calling multiple times leads to undefined behavior.
//
// See also Init / Close.
// Return client errors:
// - [ErrMissingServer]
// - [ErrNonPositiveTimeout]
//
// See also [Client.Close].
func (c *Client) Dial(prm PrmDial) error {
if prm.endpoint == "" {
panic("server address is unset or empty")
return ErrMissingServer
}
if prm.timeoutDialSet {
if prm.timeoutDial <= 0 {
panic("non-positive timeout")
return ErrNonPositiveTimeout
}
} else {
prm.timeoutDial = 5 * time.Second
@ -91,7 +101,7 @@ func (c *Client) Dial(prm PrmDial) error {
if prm.streamTimeoutSet {
if prm.streamTimeout <= 0 {
panic("non-positive timeout")
return ErrNonPositiveTimeout
}
} else {
prm.streamTimeout = 10 * time.Second
@ -134,41 +144,34 @@ func (c *Client) setNeoFSAPIServer(server neoFSAPIServer) {
// with server operations processing on running goroutines: in this case
// they are likely to fail due to a connection error.
//
// One-time method call during application shutdown stage (after Init and Dial)
// One-time method call during application shutdown stage (after [Client.Dial])
// is expected. Calling multiple times leads to undefined behavior.
//
// See also Init / Dial.
// See also [Client.Dial].
func (c *Client) Close() error {
return c.c.Conn().Close()
}
// PrmInit groups initialization parameters of Client instances.
//
// See also Init.
// See also [New].
type PrmInit struct {
resolveNeoFSErrors bool
key ecdsa.PrivateKey
signer neofscrypto.Signer
cbRespInfo func(ResponseMetaInfo) error
netMagic uint64
}
// SetDefaultPrivateKey sets Client private key to be used for the protocol
// SetDefaultSigner sets Client private signer to be used for the protocol
// communication by default.
//
// Required for operations without custom key parametrization (see corresponding Prm* docs).
func (x *PrmInit) SetDefaultPrivateKey(key ecdsa.PrivateKey) {
x.key = key
}
// ResolveNeoFSFailures makes the Client to resolve failure statuses of the
// NeoFS protocol into Go built-in errors. These errors are returned from
// each protocol operation. By default, statuses aren't resolved and written
// to the resulting structure (see corresponding Res* docs).
func (x *PrmInit) ResolveNeoFSFailures() {
x.resolveNeoFSErrors = true
// Optional if you intend to sign every request separately (see Prm* docs), but
// required if you'd like to use this signer for all operations implicitly.
// If specified, MUST be of [neofscrypto.ECDSA_DETERMINISTIC_SHA256] scheme,
// for example, [neofsecdsa.SignerRFC6979] can be used.
func (x *PrmInit) SetDefaultSigner(signer neofscrypto.Signer) {
x.signer = signer
}
// SetResponseInfoCallback makes the Client to pass ResponseMetaInfo from each

View file

@ -2,12 +2,10 @@ package client
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"testing"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
"github.com/stretchr/testify/require"
)
@ -15,28 +13,21 @@ import (
File contains common functionality used for client package testing.
*/
var key, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
var statusErr apistatus.ServerInternal
func init() {
statusErr.SetMessage("test status error")
}
func assertStatusErr(tb testing.TB, res interface{ Status() apistatus.Status }) {
require.IsType(tb, &statusErr, res.Status())
require.Equal(tb, statusErr.Message(), res.Status().(*apistatus.ServerInternal).Message())
}
func newClient(server neoFSAPIServer) *Client {
func newClient(t *testing.T, signer neofscrypto.Signer, server neoFSAPIServer) *Client {
var prm PrmInit
prm.SetDefaultPrivateKey(*key)
prm.SetDefaultSigner(signer)
var c Client
c.Init(prm)
c, err := New(prm)
require.NoError(t, err)
c.setNeoFSAPIServer(server)
return &c
return c
}
func TestClient_DialContext(t *testing.T) {

View file

@ -1,39 +1,16 @@
package client
import (
"crypto/ecdsa"
"fmt"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/signature"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
"github.com/nspcc-dev/neofs-sdk-go/version"
)
// common interface of resulting structures with API status.
type resCommon interface {
setStatus(apistatus.Status)
}
// structure is embedded to all resulting types in order to inherit status-related methods.
type statusRes struct {
st apistatus.Status
}
// setStatus implements resCommon interface method.
func (x *statusRes) setStatus(st apistatus.Status) {
x.st = st
}
// Status returns server's status return.
//
// Use apistatus package functionality to handle the status.
func (x statusRes) Status() apistatus.Status {
return x.st
}
// groups meta parameters shared between all Client operations.
type prmCommonMeta struct {
// NeoFS request X-Headers
@ -70,13 +47,6 @@ func writeXHeadersToMeta(xHeaders []string, h *v2session.RequestMetaHeader) {
h.SetXHeaders(hs)
}
// panic messages.
const (
panicMsgMissingContext = "missing context"
panicMsgMissingContainer = "missing container"
panicMsgMissingObject = "missing object"
)
// groups all the details required to send a single request and process a response to it.
type contextCall struct {
// ==================================================
@ -91,15 +61,12 @@ type contextCall struct {
// ==================================================
// shared parameters which are set uniformly on all calls
// request signing key
key ecdsa.PrivateKey
// request signer
signer neofscrypto.Signer
// callback prior to processing the response by the client
callbackResp func(ResponseMetaInfo) error
// if set, protocol errors will be expanded into a final error
resolveAPIFailures bool
// NeoFS network magic
netMagic uint64
@ -109,10 +76,7 @@ type contextCall struct {
// ==================================================
// custom call parameters
// structure of the call result
statusRes resCommon
// request to be signed with a key and sent
// request to be signed with a signer and sent
req request
// function to send a request (unary) and receive a response
@ -187,7 +151,7 @@ func (x *contextCall) writeRequest() bool {
x.req.SetVerificationHeader(nil)
// sign the request
x.err = signature.SignServiceMessage(&x.key, x.req)
x.err = signServiceMessage(x.signer, x.req)
if x.err != nil {
x.err = fmt.Errorf("sign request: %w", x.err)
return false
@ -226,44 +190,28 @@ func (x *contextCall) processResponse() bool {
// while verification needs marshaling
// verify response signature
x.err = signature.VerifyServiceMessage(x.resp)
x.err = verifyServiceMessage(x.resp)
if x.err != nil {
x.err = fmt.Errorf("invalid response signature: %w", x.err)
return false
}
// get result status
st := apistatus.FromStatusV2(x.resp.GetMetaHeader().GetStatus())
// unwrap unsuccessful status and return it
// as error if client has been configured so
successfulStatus := apistatus.IsSuccessful(st)
if x.resolveAPIFailures {
x.err = apistatus.ErrFromStatus(st)
} else {
x.statusRes.setStatus(st)
x.err = apistatus.ErrorFromV2(x.resp.GetMetaHeader().GetStatus())
return x.err == nil
}
return successfulStatus
// processResponse verifies response signature.
func (c *Client) processResponse(resp responseV2) error {
if err := verifyServiceMessage(resp); err != nil {
return fmt.Errorf("invalid response signature: %w", err)
}
// processResponse verifies response signature and converts status to an error if needed.
func (c *Client) processResponse(resp responseV2) (apistatus.Status, error) {
err := signature.VerifyServiceMessage(resp)
if err != nil {
return nil, fmt.Errorf("invalid response signature: %w", err)
}
st := apistatus.FromStatusV2(resp.GetMetaHeader().GetStatus())
if c.prm.resolveNeoFSErrors {
return st, apistatus.ErrFromStatus(st)
}
return st, nil
return apistatus.ErrorFromV2(resp.GetMetaHeader().GetStatus())
}
// reads response (if rResp is set) and processes it. Result means success.
// If failed, contextCall.err (or statusRes if resolveAPIFailures is set) contains the reason.
// If failed, contextCall.err contains the reason.
func (x *contextCall) readResponse() bool {
if x.rResp != nil {
x.err = x.rResp()
@ -328,8 +276,7 @@ func (x *contextCall) processCall() bool {
// initializes static cross-call parameters inherited from client.
func (c *Client) initCallContext(ctx *contextCall) {
ctx.key = c.prm.key
ctx.resolveAPIFailures = c.prm.resolveNeoFSErrors
ctx.signer = c.prm.signer
ctx.callbackResp = c.prm.cbRespInfo
ctx.netMagic = c.prm.netMagic
}

View file

@ -13,7 +13,6 @@ import (
"github.com/nspcc-dev/neofs-sdk-go/container"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa"
"github.com/nspcc-dev/neofs-sdk-go/eacl"
"github.com/nspcc-dev/neofs-sdk-go/session"
"github.com/nspcc-dev/neofs-sdk-go/user"
@ -28,6 +27,8 @@ type PrmContainerPut struct {
sessionSet bool
session session.Container
signer neofscrypto.Signer
}
// SetContainer sets structured information about new NeoFS container.
@ -37,6 +38,13 @@ func (x *PrmContainerPut) SetContainer(cnr container.Container) {
x.cnrSet = true
}
// SetSigner sets signer to sign request payload.
// Signer's scheme MUST be neofscrypto.ECDSA_DETERMINISTIC_SHA256. For example, you can use neofsecdsa.SignerRFC6979.
// Optional parameter: defaults to internal Client signer.
func (x *PrmContainerPut) SetSigner(signer neofscrypto.Signer) {
x.signer = signer
}
// WithinSession specifies session within which container should be saved.
//
// Creator of the session acquires the authorship of the request. This affects
@ -44,7 +52,7 @@ func (x *PrmContainerPut) SetContainer(cnr container.Container) {
//
// Session is optional, if set the following requirements apply:
// - session operation MUST be session.VerbContainerPut (ForVerb)
// - token MUST be signed using private key of the owner of the container to be saved
// - token MUST be signed using private signer of the owner of the container to be saved
func (x *PrmContainerPut) WithinSession(s session.Container) {
x.session = s
x.sessionSet = true
@ -52,8 +60,6 @@ func (x *PrmContainerPut) WithinSession(s session.Container) {
// ResContainerPut groups resulting values of ContainerPut operation.
type ResContainerPut struct {
statusRes
id cid.ID
}
@ -64,41 +70,43 @@ func (x ResContainerPut) ID() cid.ID {
return x.id
}
func (c *Client) defaultSigner() neofscrypto.Signer {
return c.prm.signer
}
// ContainerPut sends request to save container in NeoFS.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// Any errors (local or remote, including returned status codes) are returned as Go errors,
// see [apistatus] package for NeoFS-specific error types.
//
// Operation is asynchronous and no guaranteed even in the absence of errors.
// The required time is also not predictable.
//
// Success can be verified by reading by identifier (see ResContainerPut.ID).
//
// Immediately panics if parameters are set incorrectly (see PrmContainerPut docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
// Return errors:
// - [ErrMissingContainer]
func (c *Client) ContainerPut(ctx context.Context, prm PrmContainerPut) (*ResContainerPut, error) {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.cnrSet:
panic(panicMsgMissingContainer)
return nil, ErrMissingContainer
}
// TODO: check private key is set before forming the request
// TODO: check private signer is set before forming the request
// sign container
var cnr v2container.Container
prm.cnr.WriteToV2(&cnr)
var sig neofscrypto.Signature
signer := prm.signer
if signer == nil {
signer = c.defaultSigner()
}
err := container.CalculateSignature(&sig, prm.cnr, c.prm.key)
err := container.CalculateSignature(&sig, prm.cnr, signer)
if err != nil {
return nil, fmt.Errorf("calculate container signature: %w", err)
}
@ -138,7 +146,6 @@ func (c *Client) ContainerPut(ctx context.Context, prm PrmContainerPut) (*ResCon
c.initCallContext(&cc)
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.PutContainer(&c.c, &req, client.WithContext(ctx))
}
@ -184,8 +191,6 @@ func (x *PrmContainerGet) SetContainer(id cid.ID) {
// ResContainerGet groups resulting values of ContainerGet operation.
type ResContainerGet struct {
statusRes
cnr container.Container
}
@ -198,24 +203,17 @@ func (x ResContainerGet) Container() container.Container {
// ContainerGet reads NeoFS container by ID.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// Any errors (local or remote, including returned status codes) are returned as Go errors,
// see [apistatus] package for NeoFS-specific error types.
//
// Immediately panics if parameters are set incorrectly (see PrmContainerGet docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs);
// - *apistatus.ContainerNotFound.
// Return errors:
// - [ErrMissingContainer]
func (c *Client) ContainerGet(ctx context.Context, prm PrmContainerGet) (*ResContainerGet, error) {
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.idSet:
panic(panicMsgMissingContainer)
return nil, ErrMissingContainer
}
var cidV2 refs.ContainerID
@ -240,7 +238,6 @@ func (c *Client) ContainerGet(ctx context.Context, prm PrmContainerGet) (*ResCon
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.GetContainer(&c.c, &req, client.WithContext(ctx))
}
@ -284,8 +281,6 @@ func (x *PrmContainerList) SetAccount(id user.ID) {
// ResContainerList groups resulting values of ContainerList operation.
type ResContainerList struct {
statusRes
ids []cid.ID
}
@ -298,24 +293,18 @@ func (x ResContainerList) Containers() []cid.ID {
// ContainerList requests identifiers of the account-owned containers.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// Any errors (local or remote, including returned status codes) are returned as Go errors,
// see [apistatus] package for NeoFS-specific error types.
//
// Immediately panics if parameters are set incorrectly (see PrmContainerList docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
// Return errors:
// - [ErrMissingAccount]
func (c *Client) ContainerList(ctx context.Context, prm PrmContainerList) (*ResContainerList, error) {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.ownerSet:
panic("account not set")
return nil, ErrMissingAccount
}
// form request body
@ -340,7 +329,6 @@ func (c *Client) ContainerList(ctx context.Context, prm PrmContainerList) (*ResC
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.ListContainers(&c.c, &req, client.WithContext(ctx))
}
@ -375,6 +363,8 @@ type PrmContainerDelete struct {
tokSet bool
tok session.Container
signer neofscrypto.Signer
}
// SetContainer sets identifier of the NeoFS container to be removed.
@ -384,6 +374,13 @@ func (x *PrmContainerDelete) SetContainer(id cid.ID) {
x.idSet = true
}
// SetSigner sets signer to sign request payload.
// Signer's scheme MUST be neofscrypto.ECDSA_DETERMINISTIC_SHA256. For example, you can use neofsecdsa.SignerRFC6979.
// Optional parameter: defaults to internal Client signer.
func (x *PrmContainerDelete) SetSigner(signer neofscrypto.Signer) {
x.signer = signer
}
// WithinSession specifies session within which container should be removed.
//
// Creator of the session acquires the authorship of the request.
@ -395,39 +392,28 @@ func (x *PrmContainerDelete) WithinSession(tok session.Container) {
x.tokSet = true
}
// ResContainerDelete groups resulting values of ContainerDelete operation.
type ResContainerDelete struct {
statusRes
}
// ContainerDelete sends request to remove the NeoFS container.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// Any errors (local or remote, including returned status codes) are returned as Go errors,
// see [apistatus] package for NeoFS-specific error types.
//
// Operation is asynchronous and no guaranteed even in the absence of errors.
// The required time is also not predictable.
//
// Success can be verified by reading by identifier (see GetContainer).
//
// Immediately panics if parameters are set incorrectly (see PrmContainerDelete docs).
// Context is required and must not be nil. It is used for network communication.
//
// Exactly one return value is non-nil. Server status return is returned in ResContainerDelete.
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
// Return errors:
// - [ErrMissingContainer]
// - [neofscrypto.ErrIncorrectSigner]
//
// Return statuses:
// - global (see Client docs).
func (c *Client) ContainerDelete(ctx context.Context, prm PrmContainerDelete) (*ResContainerDelete, error) {
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
func (c *Client) ContainerDelete(ctx context.Context, prm PrmContainerDelete) error {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.idSet:
panic(panicMsgMissingContainer)
return ErrMissingContainer
}
// sign container ID
@ -439,10 +425,17 @@ func (c *Client) ContainerDelete(ctx context.Context, prm PrmContainerDelete) (*
data := cidV2.GetValue()
var sig neofscrypto.Signature
signer := prm.signer
if signer == nil {
signer = c.defaultSigner()
}
err := sig.Calculate(neofsecdsa.SignerRFC6979(c.prm.key), data)
if signer.Scheme() != neofscrypto.ECDSA_DETERMINISTIC_SHA256 {
return errNonNeoSigner
}
err := sig.Calculate(signer, data)
if err != nil {
return nil, fmt.Errorf("calculate signature: %w", err)
return fmt.Errorf("calculate signature: %w", err)
}
var sigv2 refs.Signature
@ -475,22 +468,20 @@ func (c *Client) ContainerDelete(ctx context.Context, prm PrmContainerDelete) (*
var (
cc contextCall
res ResContainerDelete
)
c.initCallContext(&cc)
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.DeleteContainer(&c.c, &req, client.WithContext(ctx))
}
// process call
if !cc.processCall() {
return nil, cc.err
return cc.err
}
return &res, nil
return nil
}
// PrmContainerEACL groups parameters of ContainerEACL operation.
@ -510,8 +501,6 @@ func (x *PrmContainerEACL) SetContainer(id cid.ID) {
// ResContainerEACL groups resulting values of ContainerEACL operation.
type ResContainerEACL struct {
statusRes
table eacl.Table
}
@ -522,26 +511,18 @@ func (x ResContainerEACL) Table() eacl.Table {
// ContainerEACL reads eACL table of the NeoFS container.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// Any errors (local or remote, including returned status codes) are returned as Go errors,
// see [apistatus] package for NeoFS-specific error types.
//
// Immediately panics if parameters are set incorrectly (see PrmContainerEACL docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs);
// - *apistatus.ContainerNotFound;
// - *apistatus.EACLNotFound.
// Return errors:
// - [ErrMissingContainer]
func (c *Client) ContainerEACL(ctx context.Context, prm PrmContainerEACL) (*ResContainerEACL, error) {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.idSet:
panic(panicMsgMissingContainer)
return nil, ErrMissingContainer
}
var cidV2 refs.ContainerID
@ -566,7 +547,6 @@ func (c *Client) ContainerEACL(ctx context.Context, prm PrmContainerEACL) (*ResC
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.GetEACL(&c.c, &req, client.WithContext(ctx))
}
@ -599,15 +579,24 @@ type PrmContainerSetEACL struct {
sessionSet bool
session session.Container
signer neofscrypto.Signer
}
// SetTable sets eACL table structure to be set for the container.
// Required parameter.
// Required parameter and CID must be set inside the table.
func (x *PrmContainerSetEACL) SetTable(table eacl.Table) {
x.table = table
x.tableSet = true
}
// SetSigner sets signer to sign request payload.
// Signer's scheme MUST be neofscrypto.ECDSA_DETERMINISTIC_SHA256. For example, you can use neofsecdsa.SignerRFC6979.
// Optional parameter: defaults to internal Client signer.
func (x *PrmContainerSetEACL) SetSigner(signer neofscrypto.Signer) {
x.signer = signer
}
// WithinSession specifies session within which extended ACL of the container
// should be saved.
//
@ -618,52 +607,56 @@ func (x *PrmContainerSetEACL) SetTable(table eacl.Table) {
// - if particular container is specified (ApplyOnlyTo), it MUST equal the container
// for which extended ACL is going to be set
// - session operation MUST be session.VerbContainerSetEACL (ForVerb)
// - token MUST be signed using private key of the owner of the container to be saved
// - token MUST be signed using private signer of the owner of the container to be saved
func (x *PrmContainerSetEACL) WithinSession(s session.Container) {
x.session = s
x.sessionSet = true
}
// ResContainerSetEACL groups resulting values of ContainerSetEACL operation.
type ResContainerSetEACL struct {
statusRes
}
// ContainerSetEACL sends request to update eACL table of the NeoFS container.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// Any errors (local or remote, including returned status codes) are returned as Go errors,
// see [apistatus] package for NeoFS-specific error types.
//
// Operation is asynchronous and no guaranteed even in the absence of errors.
// The required time is also not predictable.
//
// Success can be verified by reading by identifier (see EACL).
//
// Immediately panics if parameters are set incorrectly (see PrmContainerSetEACL docs).
// Context is required and must not be nil. It is used for network communication.
// Return errors:
// - [ErrMissingEACL]
// - [ErrMissingEACLContainer]
// - [neofscrypto.ErrIncorrectSigner]
//
// Return statuses:
// - global (see Client docs).
func (c *Client) ContainerSetEACL(ctx context.Context, prm PrmContainerSetEACL) (*ResContainerSetEACL, error) {
// Context is required and must not be nil. It is used for network communication.
func (c *Client) ContainerSetEACL(ctx context.Context, prm PrmContainerSetEACL) error {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.tableSet:
panic("eACL table not set")
return ErrMissingEACL
}
_, isCIDSet := prm.table.CID()
if !isCIDSet {
return ErrMissingEACLContainer
}
// sign the eACL table
eaclV2 := prm.table.ToV2()
var sig neofscrypto.Signature
signer := prm.signer
if signer == nil {
signer = c.defaultSigner()
}
err := sig.Calculate(neofsecdsa.SignerRFC6979(c.prm.key), eaclV2.StableMarshal(nil))
if signer.Scheme() != neofscrypto.ECDSA_DETERMINISTIC_SHA256 {
return errNonNeoSigner
}
err := sig.Calculate(signer, eaclV2.StableMarshal(nil))
if err != nil {
return nil, fmt.Errorf("calculate signature: %w", err)
return fmt.Errorf("calculate signature: %w", err)
}
var sigv2 refs.Signature
@ -696,22 +689,20 @@ func (c *Client) ContainerSetEACL(ctx context.Context, prm PrmContainerSetEACL)
var (
cc contextCall
res ResContainerSetEACL
)
c.initCallContext(&cc)
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.SetEACL(&c.c, &req, client.WithContext(ctx))
}
// process call
if !cc.processCall() {
return nil, cc.err
return cc.err
}
return &res, nil
return nil
}
// PrmAnnounceSpace groups parameters of ContainerAnnounceUsedSpace operation.
@ -729,36 +720,25 @@ func (x *PrmAnnounceSpace) SetValues(vs []container.SizeEstimation) {
x.announcements = vs
}
// ResAnnounceSpace groups resulting values of ContainerAnnounceUsedSpace operation.
type ResAnnounceSpace struct {
statusRes
}
// ContainerAnnounceUsedSpace sends request to announce volume of the space used for the container objects.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// Any errors (local or remote, including returned status codes) are returned as Go errors,
// see [apistatus] package for NeoFS-specific error types.
//
// Operation is asynchronous and no guaranteed even in the absence of errors.
// The required time is also not predictable.
//
// At this moment success can not be checked.
//
// Immediately panics if parameters are set incorrectly (see PrmAnnounceSpace docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
func (c *Client) ContainerAnnounceUsedSpace(ctx context.Context, prm PrmAnnounceSpace) (*ResAnnounceSpace, error) {
// Return errors:
// - [ErrMissingAnnouncements]
func (c *Client) ContainerAnnounceUsedSpace(ctx context.Context, prm PrmAnnounceSpace) error {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case len(prm.announcements) == 0:
panic("missing announcements")
return ErrMissingAnnouncements
}
// convert list of SDK announcement structures into NeoFS-API v2 list
@ -780,23 +760,21 @@ func (c *Client) ContainerAnnounceUsedSpace(ctx context.Context, prm PrmAnnounce
var (
cc contextCall
res ResAnnounceSpace
)
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.AnnounceUsedSpace(&c.c, &req, client.WithContext(ctx))
}
// process call
if !cc.processCall() {
return nil, cc.err
return cc.err
}
return &res, nil
return nil
}
// SyncContainerWithNetwork requests network configuration using passed client

View file

@ -6,21 +6,17 @@ and provides methods for executing operations on the server.
Create client instance:
var c client.Client
Initialize client state:
var prm client.PrmInit
prm.SetDefaultPrivateKey(key)
prm.SetDefaultSigner(signer)
// ...
c.Init(prm)
c, err := client.New(prm)
Connect to the NeoFS server:
var prm client.PrmDial
prm.SetServerURI("localhost:8080")
prm.SetDefaultPrivateKey(key)
prm.SetDefaultSigner(signer)
// ...
err := c.Dial(prm)

View file

@ -3,100 +3,72 @@ package client
import (
"errors"
"fmt"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
)
// unwraps err using errors.Unwrap and returns the result.
func unwrapErr(err error) error {
for e := errors.Unwrap(err); e != nil; e = errors.Unwrap(err) {
err = e
var (
// ErrMissingServer is returned when server endpoint is empty in parameters.
ErrMissingServer = errors.New("server address is unset or empty")
// ErrNonPositiveTimeout is returned when any timeout is below zero in parameters.
ErrNonPositiveTimeout = errors.New("non-positive timeout")
// ErrMissingContainer is returned when container is not provided.
ErrMissingContainer = errors.New("missing container")
// ErrMissingObject is returned when object is not provided.
ErrMissingObject = errors.New("missing object")
// ErrMissingAccount is returned when account/owner is not provided.
ErrMissingAccount = errors.New("missing account")
// ErrMissingSigner is returned when signer is not provided.
ErrMissingSigner = errors.New("missing signer")
// ErrMissingEACL is returned when eACL table is not provided.
ErrMissingEACL = errors.New("missing eACL table")
// ErrMissingEACLContainer is returned when container info is not provided in eACL table.
ErrMissingEACLContainer = errors.New("missing container in eACL table")
// ErrMissingAnnouncements is returned when announcements are not provided.
ErrMissingAnnouncements = errors.New("missing announcements")
// ErrZeroRangeLength is returned when range parameter has zero length.
ErrZeroRangeLength = errors.New("zero range length")
// ErrMissingRanges is returned when empty ranges list is provided.
ErrMissingRanges = errors.New("missing ranges")
// ErrZeroEpoch is returned when zero epoch is provided.
ErrZeroEpoch = errors.New("zero epoch")
// ErrMissingTrusts is returned when empty slice of trusts is provided.
ErrMissingTrusts = errors.New("missing trusts")
// ErrMissingTrust is returned when empty trust is not provided.
ErrMissingTrust = errors.New("missing trust")
// ErrUnexpectedReadCall is returned when we already got all data but truing to get more.
ErrUnexpectedReadCall = errors.New("unexpected call to `Read`")
// ErrSign is returned when unable to sign service message.
ErrSign SignError
// ErrMissingResponseField is returned when required field is not exists in NeoFS api response.
ErrMissingResponseField MissingResponseFieldErr
)
// MissingResponseFieldErr contains field name which should be in NeoFS API response.
type MissingResponseFieldErr struct {
name string
}
return err
// Error implements the error interface.
func (e MissingResponseFieldErr) Error() string {
return fmt.Sprintf("missing %s field in the response", e.name)
}
// IsErrContainerNotFound checks if err corresponds to NeoFS status
// return corresponding to missing container. Supports wrapped errors.
func IsErrContainerNotFound(err error) bool {
switch unwrapErr(err).(type) {
// Is implements interface for correct checking current error type with [errors.Is].
func (e MissingResponseFieldErr) Is(target error) bool {
switch target.(type) {
default:
return false
case
apistatus.ContainerNotFound,
*apistatus.ContainerNotFound:
return true
}
}
// IsErrEACLNotFound checks if err corresponds to NeoFS status
// return corresponding to missing eACL table. Supports wrapped errors.
func IsErrEACLNotFound(err error) bool {
switch unwrapErr(err).(type) {
default:
return false
case
apistatus.EACLNotFound,
*apistatus.EACLNotFound:
return true
}
}
// IsErrObjectNotFound checks if err corresponds to NeoFS status
// return corresponding to missing object. Supports wrapped errors.
func IsErrObjectNotFound(err error) bool {
switch unwrapErr(err).(type) {
default:
return false
case
apistatus.ObjectNotFound,
*apistatus.ObjectNotFound:
return true
}
}
// IsErrObjectAlreadyRemoved checks if err corresponds to NeoFS status
// return corresponding to already removed object. Supports wrapped errors.
func IsErrObjectAlreadyRemoved(err error) bool {
switch unwrapErr(err).(type) {
default:
return false
case
apistatus.ObjectAlreadyRemoved,
*apistatus.ObjectAlreadyRemoved:
return true
}
}
// IsErrSessionExpired checks if err corresponds to NeoFS status return
// corresponding to expired session. Supports wrapped errors.
func IsErrSessionExpired(err error) bool {
switch unwrapErr(err).(type) {
default:
return false
case
apistatus.SessionTokenExpired,
*apistatus.SessionTokenExpired:
return true
}
}
// IsErrSessionNotFound checks if err corresponds to NeoFS status return
// corresponding to missing session. Supports wrapped errors.
func IsErrSessionNotFound(err error) bool {
switch unwrapErr(err).(type) {
default:
return false
case
apistatus.SessionTokenNotFound,
*apistatus.SessionTokenNotFound:
case MissingResponseFieldErr, *MissingResponseFieldErr:
return true
}
}
// returns error describing missing field with the given name.
func newErrMissingResponseField(name string) error {
return fmt.Errorf("missing %s field in the response", name)
return MissingResponseFieldErr{name: name}
}
// returns error describing invalid field (according to the NeoFS protocol)
@ -104,3 +76,33 @@ func newErrMissingResponseField(name string) error {
func newErrInvalidResponseField(name string, err error) error {
return fmt.Errorf("invalid %s field in the response: %w", name, err)
}
// SignError wraps another error with reason why sign process was failed.
type SignError struct {
err error
}
// NewSignError is a constructor for [SignError].
func NewSignError(err error) SignError {
return SignError{err: err}
}
// Error implements the error interface.
func (e SignError) Error() string {
return fmt.Sprintf("sign: %v", e.err)
}
// Unwrap implements the error interface.
func (e SignError) Unwrap() error {
return e.err
}
// Is implements interface for correct checking current error type with [errors.Is].
func (e SignError) Is(target error) bool {
switch target.(type) {
default:
return false
case SignError, *SignError:
return true
}
}

View file

@ -1,68 +1,17 @@
package client_test
import (
"fmt"
"errors"
"testing"
"github.com/nspcc-dev/neofs-sdk-go/client"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"github.com/stretchr/testify/require"
)
func TestErrors(t *testing.T) {
for _, tc := range []struct {
check func(error) bool
errs []error
}{
{
check: client.IsErrContainerNotFound,
errs: []error{
apistatus.ContainerNotFound{},
new(apistatus.ContainerNotFound),
},
},
{
check: client.IsErrEACLNotFound,
errs: []error{
apistatus.EACLNotFound{},
new(apistatus.EACLNotFound),
},
},
{
check: client.IsErrObjectNotFound,
errs: []error{
apistatus.ObjectNotFound{},
new(apistatus.ObjectNotFound),
},
},
{
check: client.IsErrObjectAlreadyRemoved,
errs: []error{
apistatus.ObjectAlreadyRemoved{},
new(apistatus.ObjectAlreadyRemoved),
},
},
{
check: client.IsErrSessionExpired,
errs: []error{
apistatus.SessionTokenExpired{},
new(apistatus.SessionTokenExpired),
},
}, {
check: client.IsErrSessionNotFound,
errs: []error{
apistatus.SessionTokenNotFound{},
new(apistatus.SessionTokenNotFound),
},
},
} {
require.NotEmpty(t, tc.errs)
func Test_SignError(t *testing.T) {
someErr := errors.New("some error")
signErr := client.NewSignError(someErr)
for i := range tc.errs {
require.True(t, tc.check(tc.errs[i]), tc.errs[i])
require.True(t, tc.check(fmt.Errorf("top-level context: :%w",
fmt.Errorf("inner context: %w", tc.errs[i])),
), tc.errs[i])
}
}
require.ErrorIs(t, signErr, someErr)
require.ErrorIs(t, signErr, client.ErrSign)
}

View file

@ -0,0 +1,118 @@
package client_test
import (
"context"
"fmt"
"time"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
netmapv2 "github.com/nspcc-dev/neofs-api-go/v2/netmap"
"github.com/nspcc-dev/neofs-sdk-go/client"
"github.com/nspcc-dev/neofs-sdk-go/container"
"github.com/nspcc-dev/neofs-sdk-go/container/acl"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
"github.com/nspcc-dev/neofs-sdk-go/user"
)
func ExampleClient_ContainerPut() {
ctx := context.Background()
var accountID user.ID
// The account was taken from https://github.com/nspcc-dev/neofs-aio
key, err := keys.NEP2Decrypt("6PYM8VdX2BSm7BSXKzV4Fz6S3R9cDLLWNrD9nMjxW352jEv3fsC8N3wNLY", "one", keys.NEP2ScryptParams())
if err != nil {
panic(err)
}
signer := neofsecdsa.SignerRFC6979(key.PrivateKey)
// decode account from user's signer
if err = user.IDFromSigner(&accountID, signer); err != nil {
panic(err)
}
// prepare client
var prmInit client.PrmInit
prmInit.SetDefaultSigner(signer) // private signer for request signing
c, err := client.New(prmInit)
if err != nil {
panic(fmt.Errorf("New: %w", err))
}
// connect to NeoFS gateway
var prmDial client.PrmDial
prmDial.SetServerURI("grpc://localhost:8080") // endpoint address
prmDial.SetTimeout(15 * time.Second)
prmDial.SetStreamTimeout(15 * time.Second)
if err = c.Dial(prmDial); err != nil {
panic(fmt.Errorf("dial %v", err))
}
// describe new container
cont := container.Container{}
// set version and nonce
cont.Init()
cont.SetOwner(accountID)
cont.SetBasicACL(acl.PublicRW)
// set reserved attributes
container.SetName(&cont, "name-1")
container.SetCreationTime(&cont, time.Now().UTC())
// init placement policy
var containerID cid.ID
var placementPolicyV2 netmapv2.PlacementPolicy
var replicas []netmapv2.Replica
replica := netmapv2.Replica{}
replica.SetCount(1)
replicas = append(replicas, replica)
placementPolicyV2.SetReplicas(replicas)
var placementPolicy netmap.PlacementPolicy
if err = placementPolicy.ReadFromV2(placementPolicyV2); err != nil {
panic(fmt.Errorf("ReadFromV2 %w", err))
}
placementPolicy.SetContainerBackupFactor(1)
cont.SetPlacementPolicy(placementPolicy)
// prepare command to create container
var prmCon client.PrmContainerPut
prmCon.SetContainer(cont)
putResult, err := c.ContainerPut(ctx, prmCon)
if err != nil {
panic(fmt.Errorf("ContainerPut %w", err))
}
// containerID already exists
containerID = putResult.ID()
fmt.Println(containerID)
// example output: 76wa5UNiT8gk8Q5rdCVCV4pKuZSmYsifh6g84BcL6Hqs
// but container creation is async operation. We should wait some time or make polling to ensure container created
// for simplifying we just wait
<-time.After(2 * time.Second)
// take our created container
var cmdGet client.PrmContainerGet
cmdGet.SetContainer(containerID)
getResp, err := c.ContainerGet(ctx, cmdGet)
if err != nil {
panic(fmt.Errorf("ContainerGet %w", err))
}
jsonData, err := getResp.Container().MarshalJSON()
if err != nil {
panic(fmt.Errorf("MarshalJSON %w", err))
}
fmt.Println(string(jsonData))
// example output: {"version":{"major":2,"minor":13},"ownerID":{"value":"Ne6eoiwn40vQFI/EEI4I906PUEiy8ZXKcw=="},"nonce":"rPVd/iw2RW6Q6d66FVnIqg==","basicACL":532660223,"attributes":[{"key":"Name","value":"name-1"},{"key":"Timestamp","value":"1681738627"}],"placementPolicy":{"replicas":[{"count":1,"selector":""}],"containerBackupFactor":1,"selectors":[],"filters":[],"subnetId":{"value":0}}}
}

View file

@ -8,8 +8,6 @@ import (
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/signature"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
"github.com/nspcc-dev/neofs-sdk-go/version"
)
@ -21,8 +19,6 @@ type PrmEndpointInfo struct {
// ResEndpointInfo group resulting values of EndpointInfo operation.
type ResEndpointInfo struct {
statusRes
version version.Version
ni netmap.NodeInfo
@ -42,25 +38,14 @@ func (x ResEndpointInfo) NodeInfo() netmap.NodeInfo {
//
// Method can be used as a health check to see if node is alive and responds to requests.
//
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// Any client's internal or transport errors are returned as `error`,
// see [apistatus] package for NeoFS-specific error types.
//
// Immediately panics if parameters are set incorrectly (see PrmEndpointInfo docs).
// Context is required and must not be nil. It is used for network communication.
//
// Exactly one return value is non-nil. Server status return is returned in ResEndpointInfo.
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
//
// Return statuses:
// - global (see Client docs).
func (c *Client) EndpointInfo(ctx context.Context, prm PrmEndpointInfo) (*ResEndpointInfo, error) {
// check context
if ctx == nil {
panic(panicMsgMissingContext)
}
// form request
var req v2netmap.LocalNodeInfoRequest
@ -74,7 +59,6 @@ func (c *Client) EndpointInfo(ctx context.Context, prm PrmEndpointInfo) (*ResEnd
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.LocalNodeInfo(&c.c, &req, client.WithContext(ctx))
}
@ -127,8 +111,6 @@ type PrmNetworkInfo struct {
// ResNetworkInfo groups resulting values of NetworkInfo operation.
type ResNetworkInfo struct {
statusRes
info netmap.NetworkInfo
}
@ -139,25 +121,14 @@ func (x ResNetworkInfo) Info() netmap.NetworkInfo {
// NetworkInfo requests information about the NeoFS network of which the remote server is a part.
//
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// Any client's internal or transport errors are returned as `error`,
// see [apistatus] package for NeoFS-specific error types.
//
// Immediately panics if parameters are set incorrectly (see PrmNetworkInfo docs).
// Context is required and must not be nil. It is used for network communication.
//
// Exactly one return value is non-nil. Server status return is returned in ResNetworkInfo.
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
//
// Return statuses:
// - global (see Client docs).
func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (*ResNetworkInfo, error) {
// check context
if ctx == nil {
panic(panicMsgMissingContext)
}
// form request
var req v2netmap.NetworkInfoRequest
@ -171,7 +142,6 @@ func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (*ResNetwo
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.NetworkInfo(&c.c, &req, client.WithContext(ctx))
}
@ -207,8 +177,6 @@ type PrmNetMapSnapshot struct {
// ResNetMapSnapshot groups resulting values of NetMapSnapshot operation.
type ResNetMapSnapshot struct {
statusRes
netMap netmap.NetMap
}
@ -219,24 +187,14 @@ func (x ResNetMapSnapshot) NetMap() netmap.NetMap {
// NetMapSnapshot requests current network view of the remote server.
//
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// Any client's internal or transport errors are returned as `error`,
// see [apistatus] package for NeoFS-specific error types.
//
// Context is required and MUST NOT be nil. It is used for network communication.
//
// Exactly one return value is non-nil. Server status return is returned in ResNetMapSnapshot.
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
//
// Return statuses:
// - global (see Client docs).
func (c *Client) NetMapSnapshot(ctx context.Context, _ PrmNetMapSnapshot) (*ResNetMapSnapshot, error) {
// check context
if ctx == nil {
panic(panicMsgMissingContext)
}
// form request body
var body v2netmap.SnapshotRequestBody
@ -248,7 +206,7 @@ func (c *Client) NetMapSnapshot(ctx context.Context, _ PrmNetMapSnapshot) (*ResN
req.SetBody(&body)
c.prepareRequest(&req, &meta)
err := signature.SignServiceMessage(&c.prm.key, &req)
err := signServiceMessage(c.prm.signer, &req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
@ -259,15 +217,10 @@ func (c *Client) NetMapSnapshot(ctx context.Context, _ PrmNetMapSnapshot) (*ResN
}
var res ResNetMapSnapshot
res.st, err = c.processResponse(resp)
if err != nil {
if err = c.processResponse(resp); err != nil {
return nil, err
}
if !apistatus.IsSuccessful(res.st) {
return &res, nil
}
const fieldNetMap = "network map"
netMapV2 := resp.GetBody().NetMap()

View file

@ -7,9 +7,11 @@ import (
"testing"
v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/signature"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
"github.com/nspcc-dev/neofs-sdk-go/crypto/test"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
"github.com/stretchr/testify/require"
)
@ -23,10 +25,16 @@ type serverNetMap struct {
setNetMap bool
netMap v2netmap.NetMap
signer neofscrypto.Signer
}
func (x *serverNetMap) netMapSnapshot(ctx context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) {
err := signature.VerifyServiceMessage(&req)
func (x *serverNetMap) createSession(*client.Client, *session.CreateRequest, ...client.CallOption) (*session.CreateResponse, error) {
return nil, nil
}
func (x *serverNetMap) netMapSnapshot(_ context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) {
err := verifyServiceMessage(&req)
if err != nil {
return nil, err
}
@ -44,7 +52,7 @@ func (x *serverNetMap) netMapSnapshot(ctx context.Context, req v2netmap.Snapshot
var meta session.ResponseMetaHeader
if !x.statusOK {
meta.SetStatus(statusErr.ToStatusV2())
meta.SetStatus(statusErr.ErrorToV2())
}
var resp v2netmap.SnapshotResponse
@ -52,7 +60,7 @@ func (x *serverNetMap) netMapSnapshot(ctx context.Context, req v2netmap.Snapshot
resp.SetMetaHeader(&meta)
if x.signResponse {
err = signature.SignServiceMessage(key, &resp)
err = signServiceMessage(x.signer, &resp)
if err != nil {
panic(fmt.Sprintf("sign response: %v", err))
}
@ -66,14 +74,13 @@ func TestClient_NetMapSnapshot(t *testing.T) {
var prm PrmNetMapSnapshot
var res *ResNetMapSnapshot
var srv serverNetMap
c := newClient(&srv)
ctx := context.Background()
// missing context
require.PanicsWithValue(t, panicMsgMissingContext, func() {
//nolint:staticcheck
_, _ = c.NetMapSnapshot(nil, prm)
})
signer := test.RandomSignerRFC6979(t)
srv.signer = signer
c := newClient(t, signer, &srv)
ctx := context.Background()
// request signature
srv.errTransport = errors.New("any error")
@ -89,10 +96,10 @@ func TestClient_NetMapSnapshot(t *testing.T) {
srv.signResponse = true
// status failure
res, err = c.NetMapSnapshot(ctx, prm)
require.NoError(t, err)
assertStatusErr(t, res)
// failure error
_, err = c.NetMapSnapshot(ctx, prm)
require.Error(t, err)
require.ErrorIs(t, err, apistatus.ErrServerInternal)
srv.statusOK = true
@ -132,6 +139,5 @@ func TestClient_NetMapSnapshot(t *testing.T) {
res, err = c.NetMapSnapshot(ctx, prm)
require.NoError(t, err)
require.True(t, apistatus.IsSuccessful(res.Status()))
require.Equal(t, netMap, res.NetMap())
}

View file

@ -2,7 +2,6 @@ package client
import (
"context"
"crypto/ecdsa"
"fmt"
"github.com/nspcc-dev/neofs-api-go/v2/acl"
@ -11,10 +10,9 @@ import (
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/signature"
"github.com/nspcc-dev/neofs-sdk-go/bearer"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/session"
)
@ -28,7 +26,7 @@ type PrmObjectDelete struct {
addr v2refs.Address
keySet bool
key ecdsa.PrivateKey
signer neofscrypto.Signer
}
// WithinSession specifies session within which object should be read.
@ -56,7 +54,7 @@ func (x *PrmObjectDelete) WithBearerToken(t bearer.Token) {
}
// FromContainer specifies NeoFS container of the object.
// Required parameter.
// Required parameter. It is an alternative to [PrmObjectDelete.ByAddress].
func (x *PrmObjectDelete) FromContainer(id cid.ID) {
var cidV2 v2refs.ContainerID
id.WriteToV2(&cidV2)
@ -65,7 +63,7 @@ func (x *PrmObjectDelete) FromContainer(id cid.ID) {
}
// ByID specifies identifier of the requested object.
// Required parameter.
// Required parameter. It is an alternative to [PrmObjectDelete.ByAddress].
func (x *PrmObjectDelete) ByID(id oid.ID) {
var idV2 v2refs.ObjectID
id.WriteToV2(&idV2)
@ -73,11 +71,17 @@ func (x *PrmObjectDelete) ByID(id oid.ID) {
x.addr.SetObjectID(&idV2)
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *PrmObjectDelete) UseKey(key ecdsa.PrivateKey) {
// ByAddress specifies address of the requested object.
// Required parameter. It is an alternative to [PrmObjectDelete.ByID], [PrmObjectDelete.FromContainer].
func (x *PrmObjectDelete) ByAddress(addr oid.Address) {
addr.WriteToV2(&x.addr)
}
// UseSigner specifies private signer to sign the requests.
// If signer is not provided, then Client default signer is used.
func (x *PrmObjectDelete) UseSigner(signer neofscrypto.Signer) {
x.keySet = true
x.key = key
x.signer = signer
}
// WithXHeaders specifies list of extended headers (string key-value pairs)
@ -90,8 +94,6 @@ func (x *PrmObjectDelete) WithXHeaders(hs ...string) {
// ResObjectDelete groups resulting values of ObjectDelete operation.
type ResObjectDelete struct {
statusRes
tomb oid.ID
}
@ -110,27 +112,24 @@ func (x ResObjectDelete) Tombstone() oid.ID {
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`,
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// see [apistatus] package for NeoFS-specific error types.
//
// Immediately panics if parameters are set incorrectly (see PrmObjectDelete docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// Return errors:
// - global (see Client docs)
// - *apistatus.ContainerNotFound;
// - *apistatus.ObjectAccessDenied;
// - *apistatus.ObjectLocked;
// - *apistatus.SessionTokenExpired.
// - [ErrMissingContainer];
// - [ErrMissingObject];
// - [apistatus.ErrContainerNotFound];
// - [apistatus.ErrObjectAccessDenied];
// - [apistatus.ErrObjectLocked];
// - [apistatus.ErrSessionTokenExpired].
func (c *Client) ObjectDelete(ctx context.Context, prm PrmObjectDelete) (*ResObjectDelete, error) {
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case prm.addr.GetContainerID() == nil:
panic(panicMsgMissingContainer)
return nil, ErrMissingContainer
case prm.addr.GetObjectID() == nil:
panic(panicMsgMissingObject)
return nil, ErrMissingObject
}
// form request body
@ -141,12 +140,12 @@ func (c *Client) ObjectDelete(ctx context.Context, prm PrmObjectDelete) (*ResObj
req.SetBody(&prm.body)
c.prepareRequest(&req, &prm.meta)
key := c.prm.key
if prm.keySet {
key = prm.key
signer := prm.signer
if signer == nil {
signer = c.prm.signer
}
err := signature.SignServiceMessage(&key, &req)
err := signServiceMessage(signer, &req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
@ -157,15 +156,10 @@ func (c *Client) ObjectDelete(ctx context.Context, prm PrmObjectDelete) (*ResObj
}
var res ResObjectDelete
res.st, err = c.processResponse(resp)
if err != nil {
if err = c.processResponse(resp); err != nil {
return nil, err
}
if !apistatus.IsSuccessful(res.st) {
return &res, nil
}
const fieldTombstone = "tombstone"
idTombV2 := resp.GetBody().GetTombstone().GetObjectID()

View file

@ -0,0 +1,73 @@
package client
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"testing"
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/stretchr/testify/require"
)
func randOID(t *testing.T) oid.ID {
var id oid.ID
id.SetSHA256(randSHA256Checksum(t))
return id
}
func randCID(t *testing.T) cid.ID {
var id cid.ID
id.SetSHA256(randSHA256Checksum(t))
return id
}
func randSHA256Checksum(t *testing.T) (cs [sha256.Size]byte) {
_, err := rand.Read(cs[:])
require.NoError(t, err)
return
}
func TestPrmObjectDelete_ByAddress(t *testing.T) {
var prm PrmObjectDelete
var (
objID oid.ID
contID cid.ID
oidV2 v2refs.ObjectID
cidV2 v2refs.ContainerID
)
t.Run("ByID", func(t *testing.T) {
objID = randOID(t)
prm.ByID(objID)
objID.WriteToV2(&oidV2)
require.True(t, bytes.Equal(oidV2.GetValue(), prm.addr.GetObjectID().GetValue()))
})
t.Run("FromContainer", func(t *testing.T) {
contID = randCID(t)
prm.FromContainer(contID)
contID.WriteToV2(&cidV2)
require.True(t, bytes.Equal(cidV2.GetValue(), prm.addr.GetContainerID().GetValue()))
})
t.Run("ByAddress", func(t *testing.T) {
var addr oid.Address
addr.SetObject(objID)
addr.SetContainer(contID)
prm.ByAddress(addr)
require.True(t, bytes.Equal(oidV2.GetValue(), prm.addr.GetObjectID().GetValue()))
require.True(t, bytes.Equal(cidV2.GetValue(), prm.addr.GetContainerID().GetValue()))
})
}

View file

@ -2,7 +2,6 @@ package client
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"io"
@ -13,10 +12,9 @@ import (
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/signature"
"github.com/nspcc-dev/neofs-sdk-go/bearer"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
"github.com/nspcc-dev/neofs-sdk-go/object"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/session"
@ -73,7 +71,7 @@ func (x *prmObjectRead) WithBearerToken(t bearer.Token) {
}
// FromContainer specifies NeoFS container of the object.
// Required parameter.
// Required parameter. It is an alternative to ByAddress.
func (x *prmObjectRead) FromContainer(id cid.ID) {
var cnrV2 v2refs.ContainerID
id.WriteToV2(&cnrV2)
@ -81,23 +79,24 @@ func (x *prmObjectRead) FromContainer(id cid.ID) {
}
// ByID specifies identifier of the requested object.
// Required parameter.
// Required parameter. It is an alternative to ByAddress.
func (x *prmObjectRead) ByID(id oid.ID) {
var objV2 v2refs.ObjectID
id.WriteToV2(&objV2)
x.addr.SetObjectID(&objV2)
}
// ByAddress specifies address of the requested object.
// Required parameter. It is an alternative to ByID, FromContainer.
func (x *prmObjectRead) ByAddress(addr oid.Address) {
addr.WriteToV2(&x.addr)
}
// PrmObjectGet groups parameters of ObjectGetInit operation.
type PrmObjectGet struct {
prmObjectRead
key *ecdsa.PrivateKey
}
// ResObjectGet groups the final result values of ObjectGetInit operation.
type ResObjectGet struct {
statusRes
signer neofscrypto.Signer
}
// ObjectReader is designed to read one object from NeoFS system.
@ -112,7 +111,6 @@ type ObjectReader struct {
Read(resp *v2object.GetResponse) error
}
res ResObjectGet
err error
tailPayload []byte
@ -120,10 +118,10 @@ type ObjectReader struct {
remainingPayloadLen int
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *PrmObjectGet) UseKey(key ecdsa.PrivateKey) {
x.key = &key
// UseSigner specifies private signer to sign the requests.
// If signer is not provided, then Client default signer is used.
func (x *PrmObjectGet) UseSigner(signer neofscrypto.Signer) {
x.signer = signer
}
// ReadHeader reads header of the object. Result means success.
@ -135,8 +133,8 @@ func (x *ObjectReader) ReadHeader(dst *object.Object) bool {
return false
}
x.res.st, x.err = x.client.processResponse(&resp)
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
x.err = x.client.processResponse(&resp)
if x.err != nil {
return false
}
@ -188,8 +186,8 @@ func (x *ObjectReader) readChunk(buf []byte) (int, bool) {
return read, false
}
x.res.st, x.err = x.client.processResponse(&resp)
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
x.err = x.client.processResponse(&resp)
if x.err != nil {
return read, false
}
@ -228,44 +226,40 @@ func (x *ObjectReader) ReadChunk(buf []byte) (int, bool) {
return x.readChunk(buf)
}
func (x *ObjectReader) close(ignoreEOF bool) (*ResObjectGet, error) {
func (x *ObjectReader) close(ignoreEOF bool) error {
defer x.cancelCtxStream()
if x.err != nil {
if !errors.Is(x.err, io.EOF) {
return nil, x.err
return x.err
} else if !ignoreEOF {
if x.remainingPayloadLen > 0 {
return nil, io.ErrUnexpectedEOF
return io.ErrUnexpectedEOF
}
return nil, io.EOF
return io.EOF
}
}
return &x.res, nil
return nil
}
// Close ends reading the object and returns the result of the operation
// along with the final results. Must be called after using the ObjectReader.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as Go built-in error.
// If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
// codes are returned as error.
//
// Return errors:
//
// *object.SplitInfoError (returned on virtual objects with PrmObjectGet.MakeRaw).
//
// Return statuses:
// - global (see Client docs);
// - *apistatus.ContainerNotFound;
// - *apistatus.ObjectNotFound;
// - *apistatus.ObjectAccessDenied;
// - *apistatus.ObjectAlreadyRemoved;
// - *apistatus.SessionTokenExpired.
func (x *ObjectReader) Close() (*ResObjectGet, error) {
// - *[object.SplitInfoError] (returned on virtual objects with PrmObjectGet.MakeRaw).
// - [apistatus.ErrContainerNotFound];
// - [apistatus.ErrObjectNotFound];
// - [apistatus.ErrObjectAccessDenied];
// - [apistatus.ErrObjectAlreadyRemoved];
// - [apistatus.ErrSessionTokenExpired].
func (x *ObjectReader) Close() error {
return x.close(true)
}
@ -276,12 +270,11 @@ func (x *ObjectReader) Read(p []byte) (int, error) {
x.remainingPayloadLen -= n
if !ok {
res, err := x.close(false)
if err != nil {
if err := x.close(false); err != nil {
return n, err
}
return n, apistatus.ErrFromStatus(res.Status())
return n, x.err
}
if x.remainingPayloadLen < 0 {
@ -296,17 +289,18 @@ func (x *ObjectReader) Read(p []byte) (int, error) {
// The call only opens the transmission channel, explicit fetching is done using the ObjectReader.
// Exactly one return value is non-nil. Resulting reader must be finally closed.
//
// Immediately panics if parameters are set incorrectly (see PrmObjectGet docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return errors:
// - [ErrMissingContainer]
// - [ErrMissingObject]
func (c *Client) ObjectGetInit(ctx context.Context, prm PrmObjectGet) (*ObjectReader, error) {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case prm.addr.GetContainerID() == nil:
panic(panicMsgMissingContainer)
return nil, ErrMissingContainer
case prm.addr.GetObjectID() == nil:
panic(panicMsgMissingObject)
return nil, ErrMissingObject
}
// form request body
@ -321,12 +315,12 @@ func (c *Client) ObjectGetInit(ctx context.Context, prm PrmObjectGet) (*ObjectRe
req.SetBody(&body)
c.prepareRequest(&req, &prm.meta)
key := prm.key
if key == nil {
key = &c.prm.key
signer := prm.signer
if signer == nil {
signer = c.prm.signer
}
err := signature.SignServiceMessage(key, &req)
err := signServiceMessage(signer, &req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
@ -351,21 +345,17 @@ func (c *Client) ObjectGetInit(ctx context.Context, prm PrmObjectGet) (*ObjectRe
type PrmObjectHead struct {
prmObjectRead
keySet bool
key ecdsa.PrivateKey
signer neofscrypto.Signer
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *PrmObjectHead) UseKey(key ecdsa.PrivateKey) {
x.keySet = true
x.key = key
// UseSigner specifies private signer to sign the requests.
// If signer is not provided, then Client default signer is used.
func (x *PrmObjectHead) UseSigner(signer neofscrypto.Signer) {
x.signer = signer
}
// ResObjectHead groups resulting values of ObjectHead operation.
type ResObjectHead struct {
statusRes
// requested object (response doesn't carry the ID)
idObj oid.ID
@ -396,32 +386,26 @@ func (x *ResObjectHead) ReadHeader(dst *object.Object) bool {
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`,
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// see [apistatus] package for NeoFS-specific error types.
//
// Immediately panics if parameters are set incorrectly (see PrmObjectHead docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return errors:
//
// *object.SplitInfoError (returned on virtual objects with PrmObjectHead.MakeRaw).
//
// Return statuses:
// - global (see Client docs);
// - *apistatus.ContainerNotFound;
// - *apistatus.ObjectNotFound;
// - *apistatus.ObjectAccessDenied;
// - *apistatus.ObjectAlreadyRemoved;
// - *apistatus.SessionTokenExpired.
// - [ErrMissingContainer];
// - [ErrMissingObject];
// - *[object.SplitInfoError] (returned on virtual objects with PrmObjectHead.MakeRaw).
// - [apistatus.ErrContainerNotFound];
// - [apistatus.ErrObjectNotFound];
// - [apistatus.ErrObjectAccessDenied];
// - [apistatus.ErrObjectAlreadyRemoved];
// - [apistatus.ErrSessionTokenExpired].
func (c *Client) ObjectHead(ctx context.Context, prm PrmObjectHead) (*ResObjectHead, error) {
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case prm.addr.GetContainerID() == nil:
panic(panicMsgMissingContainer)
return nil, ErrMissingContainer
case prm.addr.GetObjectID() == nil:
panic(panicMsgMissingObject)
return nil, ErrMissingObject
}
var body v2object.HeadRequestBody
@ -432,13 +416,13 @@ func (c *Client) ObjectHead(ctx context.Context, prm PrmObjectHead) (*ResObjectH
req.SetBody(&body)
c.prepareRequest(&req, &prm.meta)
key := c.prm.key
if prm.keySet {
key = prm.key
signer := prm.signer
if signer == nil {
signer = c.prm.signer
}
// sign the request
err := signature.SignServiceMessage(&key, &req)
err := signServiceMessage(signer, &req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
@ -449,15 +433,10 @@ func (c *Client) ObjectHead(ctx context.Context, prm PrmObjectHead) (*ResObjectH
}
var res ResObjectHead
res.st, err = c.processResponse(resp)
if err != nil {
if err = c.processResponse(resp); err != nil {
return nil, err
}
if !apistatus.IsSuccessful(res.st) {
return &res, nil
}
_ = res.idObj.ReadFromV2(*prm.addr.GetObjectID())
switch v := resp.GetBody().GetHeaderPart().(type) {
@ -476,32 +455,33 @@ func (c *Client) ObjectHead(ctx context.Context, prm PrmObjectHead) (*ResObjectH
type PrmObjectRange struct {
prmObjectRead
key *ecdsa.PrivateKey
signer neofscrypto.Signer
rng v2object.Range
}
// SetOffset sets offset of the payload range to be read.
// Zero by default.
// Zero by default. It is an alternative to [PrmObjectRange.SetRange].
func (x *PrmObjectRange) SetOffset(off uint64) {
x.rng.SetOffset(off)
}
// SetLength sets length of the payload range to be read.
// Must be positive.
// Must be positive. It is an alternative to [PrmObjectRange.SetRange].
func (x *PrmObjectRange) SetLength(ln uint64) {
x.rng.SetLength(ln)
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *PrmObjectRange) UseKey(key ecdsa.PrivateKey) {
x.key = &key
// SetRange sets range of the payload to be read.
// It is an alternative to [PrmObjectRange.SetOffset], [PrmObjectRange.SetLength].
func (x *PrmObjectRange) SetRange(rng object.Range) {
x.rng = *rng.ToV2()
}
// ResObjectRange groups the final result values of ObjectRange operation.
type ResObjectRange struct {
statusRes
// UseSigner specifies private signer to sign the requests.
// If signer is not provided, then Client default signer is used.
func (x *PrmObjectRange) UseSigner(signer neofscrypto.Signer) {
x.signer = signer
}
// ObjectRangeReader is designed to read payload range of one object
@ -514,7 +494,6 @@ type ObjectRangeReader struct {
client *Client
res ResObjectRange
err error
stream interface {
@ -549,8 +528,8 @@ func (x *ObjectRangeReader) readChunk(buf []byte) (int, bool) {
return read, false
}
x.res.st, x.err = x.client.processResponse(&resp)
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
x.err = x.client.processResponse(&resp)
if x.err != nil {
return read, false
}
@ -593,45 +572,41 @@ func (x *ObjectRangeReader) ReadChunk(buf []byte) (int, bool) {
return x.readChunk(buf)
}
func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, error) {
func (x *ObjectRangeReader) close(ignoreEOF bool) error {
defer x.cancelCtxStream()
if x.err != nil {
if !errors.Is(x.err, io.EOF) {
return nil, x.err
return x.err
} else if !ignoreEOF {
if x.remainingPayloadLen > 0 {
return nil, io.ErrUnexpectedEOF
return io.ErrUnexpectedEOF
}
return nil, io.EOF
return io.EOF
}
}
return &x.res, nil
return nil
}
// Close ends reading the payload range and returns the result of the operation
// along with the final results. Must be called after using the ObjectRangeReader.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as Go built-in error.
// If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
// codes are returned as error.
//
// Return errors:
//
// *object.SplitInfoError (returned on virtual objects with PrmObjectRange.MakeRaw).
//
// Return statuses:
// - global (see Client docs);
// - *apistatus.ContainerNotFound;
// - *apistatus.ObjectNotFound;
// - *apistatus.ObjectAccessDenied;
// - *apistatus.ObjectAlreadyRemoved;
// - *apistatus.ObjectOutOfRange;
// - *apistatus.SessionTokenExpired.
func (x *ObjectRangeReader) Close() (*ResObjectRange, error) {
// - *[object.SplitInfoError] (returned on virtual objects with PrmObjectRange.MakeRaw).
// - [apistatus.ErrContainerNotFound];
// - [apistatus.ErrObjectNotFound];
// - [apistatus.ErrObjectAccessDenied];
// - [apistatus.ErrObjectAlreadyRemoved];
// - [apistatus.ErrObjectOutOfRange];
// - [apistatus.ErrSessionTokenExpired].
func (x *ObjectRangeReader) Close() error {
return x.close(true)
}
@ -642,12 +617,12 @@ func (x *ObjectRangeReader) Read(p []byte) (int, error) {
x.remainingPayloadLen -= n
if !ok {
res, err := x.close(false)
err := x.close(false)
if err != nil {
return n, err
}
return n, apistatus.ErrFromStatus(res.Status())
return n, x.err
}
if x.remainingPayloadLen < 0 {
@ -663,19 +638,21 @@ func (x *ObjectRangeReader) Read(p []byte) (int, error) {
// The call only opens the transmission channel, explicit fetching is done using the ObjectRangeReader.
// Exactly one return value is non-nil. Resulting reader must be finally closed.
//
// Immediately panics if parameters are set incorrectly (see PrmObjectRange docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return errors:
// - [ErrMissingContainer]
// - [ErrMissingObject]
// - [ErrZeroRangeLength]
func (c *Client) ObjectRangeInit(ctx context.Context, prm PrmObjectRange) (*ObjectRangeReader, error) {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case prm.addr.GetContainerID() == nil:
panic(panicMsgMissingContainer)
return nil, ErrMissingContainer
case prm.addr.GetObjectID() == nil:
panic(panicMsgMissingObject)
return nil, ErrMissingObject
case prm.rng.GetLength() == 0:
panic("zero range length")
return nil, ErrZeroRangeLength
}
// form request body
@ -691,12 +668,12 @@ func (c *Client) ObjectRangeInit(ctx context.Context, prm PrmObjectRange) (*Obje
req.SetBody(&body)
c.prepareRequest(&req, &prm.meta)
key := prm.key
if key == nil {
key = &c.prm.key
signer := prm.signer
if signer == nil {
signer = c.prm.signer
}
err := signature.SignServiceMessage(key, &req)
err := signServiceMessage(signer, &req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}

86
client/object_get_test.go Normal file
View file

@ -0,0 +1,86 @@
package client
import (
"bytes"
"math/rand"
"testing"
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"github.com/nspcc-dev/neofs-sdk-go/object"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/stretchr/testify/require"
)
func TestPrmObjectRead_ByAddress(t *testing.T) {
var prm PrmObjectHead
var (
objID oid.ID
contID cid.ID
oidV2 v2refs.ObjectID
cidV2 v2refs.ContainerID
)
t.Run("ByID", func(t *testing.T) {
objID = randOID(t)
prm.ByID(objID)
objID.WriteToV2(&oidV2)
require.True(t, bytes.Equal(oidV2.GetValue(), prm.addr.GetObjectID().GetValue()))
})
t.Run("FromContainer", func(t *testing.T) {
contID = randCID(t)
prm.FromContainer(contID)
contID.WriteToV2(&cidV2)
require.True(t, bytes.Equal(cidV2.GetValue(), prm.addr.GetContainerID().GetValue()))
})
t.Run("ByAddress", func(t *testing.T) {
var addr oid.Address
addr.SetObject(objID)
addr.SetContainer(contID)
prm.ByAddress(addr)
require.True(t, bytes.Equal(oidV2.GetValue(), prm.addr.GetObjectID().GetValue()))
require.True(t, bytes.Equal(cidV2.GetValue(), prm.addr.GetContainerID().GetValue()))
})
}
func TestPrmObjectRange_SetRange(t *testing.T) {
var prm PrmObjectRange
var (
ln = rand.Uint64()
off = rand.Uint64()
rng *object.Range
)
t.Run("SetLength", func(t *testing.T) {
prm.SetLength(ln)
rng = object.NewRangeFromV2(&prm.rng)
require.Equal(t, ln, rng.GetLength())
})
t.Run("SetOffset", func(t *testing.T) {
prm.SetOffset(off)
rng = object.NewRangeFromV2(&prm.rng)
require.Equal(t, off, rng.GetOffset())
})
t.Run("SetRange", func(t *testing.T) {
var tmp object.Range
tmp.SetLength(ln)
tmp.SetOffset(off)
prm.SetRange(tmp)
require.Equal(t, ln, tmp.ToV2().GetLength())
require.Equal(t, off, tmp.ToV2().GetOffset())
})
}

View file

@ -10,10 +10,9 @@ import (
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/signature"
"github.com/nspcc-dev/neofs-sdk-go/bearer"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/session"
)
@ -27,6 +26,14 @@ type PrmObjectHash struct {
csAlgo v2refs.ChecksumType
addr v2refs.Address
signer neofscrypto.Signer
}
// UseSigner specifies private signer to sign the requests.
// If signer is not provided, then Client default signer is used.
func (x *PrmObjectHash) UseSigner(signer neofscrypto.Signer) {
x.signer = signer
}
// MarkLocal tells the server to execute the operation locally.
@ -59,7 +66,7 @@ func (x *PrmObjectHash) WithBearerToken(t bearer.Token) {
}
// FromContainer specifies NeoFS container of the object.
// Required parameter.
// Required parameter. It is an alternative to [PrmObjectHash.ByAddress].
func (x *PrmObjectHash) FromContainer(id cid.ID) {
var cidV2 v2refs.ContainerID
id.WriteToV2(&cidV2)
@ -68,7 +75,7 @@ func (x *PrmObjectHash) FromContainer(id cid.ID) {
}
// ByID specifies identifier of the requested object.
// Required parameter.
// Required parameter. It is an alternative to [PrmObjectHash.ByAddress].
func (x *PrmObjectHash) ByID(id oid.ID) {
var idV2 v2refs.ObjectID
id.WriteToV2(&idV2)
@ -76,6 +83,12 @@ func (x *PrmObjectHash) ByID(id oid.ID) {
x.addr.SetObjectID(&idV2)
}
// ByAddress specifies address of the requested object.
// Required parameter. It is an alternative to [PrmObjectHash.ByID], [PrmObjectHash.FromContainer].
func (x *PrmObjectHash) ByAddress(addr oid.Address) {
addr.WriteToV2(&x.addr)
}
// SetRangeList sets list of ranges in (offset, length) pair format.
// Required parameter.
//
@ -121,8 +134,6 @@ func (x *PrmObjectHash) WithXHeaders(hs ...string) {
// ResObjectHash groups resulting values of ObjectHash operation.
type ResObjectHash struct {
statusRes
checksums [][]byte
}
@ -139,30 +150,22 @@ func (x ResObjectHash) Checksums() [][]byte {
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`,
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// see [apistatus] package for NeoFS-specific error types.
//
// Immediately panics if parameters are set incorrectly (see PrmObjectHash docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs);
// - *apistatus.ContainerNotFound;
// - *apistatus.ObjectNotFound;
// - *apistatus.ObjectAccessDenied;
// - *apistatus.ObjectOutOfRange;
// - *apistatus.SessionTokenExpired.
// Return errors:
// - [ErrMissingContainer]
// - [ErrMissingObject]
// - [ErrMissingRanges]
func (c *Client) ObjectHash(ctx context.Context, prm PrmObjectHash) (*ResObjectHash, error) {
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case prm.addr.GetContainerID() == nil:
panic(panicMsgMissingContainer)
return nil, ErrMissingContainer
case prm.addr.GetObjectID() == nil:
panic(panicMsgMissingObject)
return nil, ErrMissingObject
case len(prm.body.GetRanges()) == 0:
panic("missing ranges")
return nil, ErrMissingRanges
}
prm.body.SetAddress(&prm.addr)
@ -176,7 +179,12 @@ func (c *Client) ObjectHash(ctx context.Context, prm PrmObjectHash) (*ResObjectH
c.prepareRequest(&req, &prm.meta)
req.SetBody(&prm.body)
err := signature.SignServiceMessage(&c.prm.key, &req)
signer := prm.signer
if signer == nil {
signer = c.prm.signer
}
err := signServiceMessage(signer, &req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
@ -187,15 +195,10 @@ func (c *Client) ObjectHash(ctx context.Context, prm PrmObjectHash) (*ResObjectH
}
var res ResObjectHash
res.st, err = c.processResponse(resp)
if err != nil {
if err = c.processResponse(resp); err != nil {
return nil, err
}
if !apistatus.IsSuccessful(res.st) {
return &res, nil
}
res.checksums = resp.GetBody().GetHashList()
if len(res.checksums) == 0 {
return nil, newErrMissingResponseField("hash list")

View file

@ -0,0 +1,50 @@
package client
import (
"bytes"
"testing"
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/stretchr/testify/require"
)
func TestPrmObjectHash_ByAddress(t *testing.T) {
var prm PrmObjectHash
var (
objID oid.ID
contID cid.ID
oidV2 v2refs.ObjectID
cidV2 v2refs.ContainerID
)
t.Run("ByID", func(t *testing.T) {
objID = randOID(t)
prm.ByID(objID)
objID.WriteToV2(&oidV2)
require.True(t, bytes.Equal(oidV2.GetValue(), prm.addr.GetObjectID().GetValue()))
})
t.Run("FromContainer", func(t *testing.T) {
contID = randCID(t)
prm.FromContainer(contID)
contID.WriteToV2(&cidV2)
require.True(t, bytes.Equal(cidV2.GetValue(), prm.addr.GetContainerID().GetValue()))
})
t.Run("ByAddress", func(t *testing.T) {
var addr oid.Address
addr.SetObject(objID)
addr.SetContainer(contID)
prm.ByAddress(addr)
require.True(t, bytes.Equal(oidV2.GetValue(), prm.addr.GetObjectID().GetValue()))
require.True(t, bytes.Equal(cidV2.GetValue(), prm.addr.GetContainerID().GetValue()))
})
}

View file

@ -2,7 +2,6 @@ package client
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"io"
@ -12,18 +11,20 @@ import (
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/signature"
"github.com/nspcc-dev/neofs-sdk-go/bearer"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
"github.com/nspcc-dev/neofs-sdk-go/object"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/object/slicer"
"github.com/nspcc-dev/neofs-sdk-go/session"
"github.com/nspcc-dev/neofs-sdk-go/user"
)
// PrmObjectPutInit groups parameters of ObjectPutInit operation.
type PrmObjectPutInit struct {
copyNum uint32
key *ecdsa.PrivateKey
signer neofscrypto.Signer
meta v2session.RequestMetaHeader
}
@ -34,8 +35,6 @@ func (x *PrmObjectPutInit) SetCopiesNumber(copiesNumber uint32) {
// ResObjectPut groups the final result values of ObjectPutInit operation.
type ResObjectPut struct {
statusRes
obj oid.ID
}
@ -57,7 +56,7 @@ type ObjectWriter struct {
Close() error
}
key *ecdsa.PrivateKey
signer neofscrypto.Signer
res ResObjectPut
err error
@ -69,10 +68,10 @@ type ObjectWriter struct {
partChunk v2object.PutObjectPartChunk
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *PrmObjectPutInit) UseKey(key ecdsa.PrivateKey) {
x.key = &key
// UseSigner specifies private signer to sign the requests.
// If signer is not provided, then Client default signer is used.
func (x *PrmObjectPutInit) UseSigner(signer neofscrypto.Signer) {
x.signer = signer
}
// WithBearerToken attaches bearer token to be used for the operation.
@ -117,7 +116,7 @@ func (x *ObjectWriter) WriteHeader(hdr object.Object) bool {
x.req.GetBody().SetObjectPart(&x.partInit)
x.req.SetVerificationHeader(nil)
x.err = signature.SignServiceMessage(x.key, &x.req)
x.err = signServiceMessage(x.signer, &x.req)
if x.err != nil {
x.err = fmt.Errorf("sign message: %w", x.err)
return false
@ -159,7 +158,7 @@ func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool {
x.partChunk.SetChunk(chunk[:ln])
x.req.SetVerificationHeader(nil)
x.err = signature.SignServiceMessage(x.key, &x.req)
x.err = signServiceMessage(x.signer, &x.req)
if x.err != nil {
x.err = fmt.Errorf("sign message: %w", x.err)
return false
@ -184,14 +183,14 @@ func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool {
// If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
// codes are returned as error.
//
// Return statuses:
// Return errors:
// - global (see Client docs);
// - *apistatus.ContainerNotFound;
// - *apistatus.ObjectAccessDenied;
// - *apistatus.ObjectLocked;
// - *apistatus.LockNonRegularObject;
// - *apistatus.SessionTokenNotFound;
// - *apistatus.SessionTokenExpired.
// - [apistatus.ErrContainerNotFound];
// - [apistatus.ErrObjectAccessDenied];
// - [apistatus.ErrObjectLocked];
// - [apistatus.ErrLockNonRegularObject];
// - [apistatus.ErrSessionTokenNotFound];
// - [apistatus.ErrSessionTokenExpired].
func (x *ObjectWriter) Close() (*ResObjectPut, error) {
defer x.cancelCtxStream()
@ -206,15 +205,10 @@ func (x *ObjectWriter) Close() (*ResObjectPut, error) {
return nil, x.err
}
x.res.st, x.err = x.client.processResponse(&x.respV2)
if x.err != nil {
if x.err = x.client.processResponse(&x.respV2); x.err != nil {
return nil, x.err
}
if !apistatus.IsSuccessful(x.res.st) {
return &x.res, nil
}
const fieldID = "ID"
idV2 := x.respV2.GetBody().GetObjectID()
@ -237,11 +231,6 @@ func (x *ObjectWriter) Close() (*ResObjectPut, error) {
//
// Context is required and must not be nil. It is used for network communication.
func (c *Client) ObjectPutInit(ctx context.Context, prm PrmObjectPutInit) (*ObjectWriter, error) {
// check parameters
if ctx == nil {
panic(panicMsgMissingContext)
}
var w ObjectWriter
ctx, cancel := context.WithCancel(ctx)
@ -251,9 +240,9 @@ func (c *Client) ObjectPutInit(ctx context.Context, prm PrmObjectPutInit) (*Obje
return nil, fmt.Errorf("open stream: %w", err)
}
w.key = &c.prm.key
if prm.key != nil {
w.key = prm.key
w.signer = prm.signer
if w.signer == nil {
w.signer = c.prm.signer
}
w.cancelCtxStream = cancel
w.client = c
@ -264,3 +253,106 @@ func (c *Client) ObjectPutInit(ctx context.Context, prm PrmObjectPutInit) (*Obje
return &w, nil
}
type objectWriter struct {
context context.Context
client *Client
}
func (x *objectWriter) InitDataStream(header object.Object) (io.Writer, error) {
var prm PrmObjectPutInit
stream, err := x.client.ObjectPutInit(x.context, prm)
if err != nil {
return nil, fmt.Errorf("init object stream: %w", err)
}
if stream.WriteHeader(header) {
return &payloadWriter{
stream: stream,
}, nil
}
_, err = stream.Close()
if err != nil {
return nil, err
}
return nil, errors.New("unexpected error")
}
type payloadWriter struct {
stream *ObjectWriter
}
func (x *payloadWriter) Write(p []byte) (int, error) {
if !x.stream.WritePayloadChunk(p) {
return 0, x.Close()
}
return len(p), nil
}
func (x *payloadWriter) Close() error {
_, err := x.stream.Close()
if err != nil {
return err
}
return nil
}
// CreateObject creates new NeoFS object with given payload data and stores it
// in specified container of the NeoFS network using provided Client connection.
// The object is created on behalf of provided neofscrypto.Signer, and owned by
// the specified user.ID.
//
// In terms of NeoFS, parameterized neofscrypto.Signer represents object owner,
// object signer and request sender. Container SHOULD be public-write or sender
// SHOULD have corresponding rights.
//
// Client connection MUST be opened in advance, see Dial method for details.
// Network communication is carried out within a given context, so it MUST NOT
// be nil.
//
// Notice: This API is EXPERIMENTAL and is planned to be replaced/changed in the
// future. Be ready to refactor your code regarding imports and call mechanics,
// in essence the operation will not change.
func CreateObject(ctx context.Context, cli *Client, signer neofscrypto.Signer, cnr cid.ID, owner user.ID, data io.Reader, attributes ...string) (oid.ID, error) {
s, err := NewDataSlicer(ctx, cli, signer, cnr, owner)
if err != nil {
return oid.ID{}, err
}
return s.Slice(data, attributes...)
}
// NewDataSlicer creates slicer.Slicer that saves data in the NeoFS network
// through provided Client. The data is packaged into NeoFS objects stored in
// the specified container. Provided signer is being used to sign the resulting
// objects as a system requirement. Produced objects are owned by the
// parameterized NeoFS user.
//
// Notice: This API is EXPERIMENTAL and is planned to be replaced/changed in the
// future. Be ready to refactor your code regarding imports and call mechanics,
// in essence the operation will not change.
func NewDataSlicer(ctx context.Context, cli *Client, signer neofscrypto.Signer, cnr cid.ID, owner user.ID) (*slicer.Slicer, error) {
resNetInfo, err := cli.NetworkInfo(ctx, PrmNetworkInfo{})
if err != nil {
return nil, fmt.Errorf("read current network info: %w", err)
}
netInfo := resNetInfo.Info()
var opts slicer.Options
opts.SetObjectPayloadLimit(netInfo.MaxObjectSize())
opts.SetCurrentNeoFSEpoch(netInfo.CurrentEpoch())
if !netInfo.HomomorphicHashingDisabled() {
opts.CalculateHomomorphicChecksum()
}
return slicer.New(signer, cnr, owner, &objectWriter{
context: ctx,
client: cli,
}, opts), nil
}

View file

@ -2,7 +2,6 @@ package client
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"io"
@ -13,10 +12,9 @@ import (
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/signature"
"github.com/nspcc-dev/neofs-sdk-go/bearer"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
"github.com/nspcc-dev/neofs-sdk-go/object"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/session"
@ -26,7 +24,7 @@ import (
type PrmObjectSearch struct {
meta v2session.RequestMetaHeader
key *ecdsa.PrivateKey
signer neofscrypto.Signer
cnrSet bool
cnrID cid.ID
@ -70,10 +68,10 @@ func (x *PrmObjectSearch) WithXHeaders(hs ...string) {
writeXHeadersToMeta(hs, &x.meta)
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *PrmObjectSearch) UseKey(key ecdsa.PrivateKey) {
x.key = &key
// UseSigner specifies private signer to sign the requests.
// If signer is not provided, then Client default signer is used.
func (x *PrmObjectSearch) UseSigner(signer neofscrypto.Signer) {
x.signer = signer
}
// InContainer specifies the container in which to look for objects.
@ -89,11 +87,6 @@ func (x *PrmObjectSearch) SetFilters(filters object.SearchFilters) {
x.filters = filters
}
// ResObjectSearch groups the final result values of ObjectSearch operation.
type ResObjectSearch struct {
statusRes
}
// ObjectListReader is designed to read list of object identifiers from NeoFS system.
//
// Must be initialized using Client.ObjectSearch, any other usage is unsafe.
@ -101,7 +94,6 @@ type ObjectListReader struct {
client *Client
cancelCtxStream context.CancelFunc
err error
res ResObjectSearch
stream interface {
Read(resp *v2object.SearchResponse) error
}
@ -133,8 +125,8 @@ func (x *ObjectListReader) Read(buf []oid.ID) (int, bool) {
return read, false
}
x.res.st, x.err = x.client.processResponse(&resp)
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
x.err = x.client.processResponse(&resp)
if x.err != nil {
return read, false
}
@ -177,11 +169,7 @@ func (x *ObjectListReader) Iterate(f func(oid.ID) bool) error {
// so false means nothing was read.
_, ok := x.Read(buf)
if !ok {
res, err := x.Close()
if err != nil {
return err
}
return apistatus.ErrFromStatus(res.Status())
return x.Close()
}
if f(buf[0]) {
return nil
@ -192,24 +180,23 @@ func (x *ObjectListReader) Iterate(f func(oid.ID) bool) error {
// Close ends reading list of the matched objects and returns the result of the operation
// along with the final results. Must be called after using the ObjectListReader.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as Go built-in error.
// If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
// codes are returned as error.
//
// Return statuses:
// Return errors:
// - global (see Client docs);
// - *apistatus.ContainerNotFound;
// - *apistatus.ObjectAccessDenied;
// - *apistatus.SessionTokenExpired.
func (x *ObjectListReader) Close() (*ResObjectSearch, error) {
// - [apistatus.ErrContainerNotFound];
// - [apistatus.ErrObjectAccessDenied];
// - [apistatus.ErrSessionTokenExpired].
func (x *ObjectListReader) Close() error {
defer x.cancelCtxStream()
if x.err != nil && !errors.Is(x.err, io.EOF) {
return nil, x.err
return x.err
}
return &x.res, nil
return nil
}
// ObjectSearchInit initiates object selection through a remote server using NeoFS API protocol.
@ -218,15 +205,15 @@ func (x *ObjectListReader) Close() (*ResObjectSearch, error) {
// is done using the ObjectListReader. Exactly one return value is non-nil.
// Resulting reader must be finally closed.
//
// Immediately panics if parameters are set incorrectly (see PrmObjectSearch docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return errors:
// - [ErrMissingContainer]
func (c *Client) ObjectSearchInit(ctx context.Context, prm PrmObjectSearch) (*ObjectListReader, error) {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.cnrSet:
panic(panicMsgMissingContainer)
return nil, ErrMissingContainer
}
var cidV2 v2refs.ContainerID
@ -242,12 +229,12 @@ func (c *Client) ObjectSearchInit(ctx context.Context, prm PrmObjectSearch) (*Ob
req.SetBody(&body)
c.prepareRequest(&req, &prm.meta)
key := prm.key
if key == nil {
key = &c.prm.key
signer := prm.signer
if signer == nil {
signer = c.prm.signer
}
err := signature.SignServiceMessage(key, &req)
err := signServiceMessage(signer, &req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}

View file

@ -1,16 +1,14 @@
package client
import (
"crypto/ecdsa"
"errors"
"fmt"
"io"
"testing"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
signatureV2 "github.com/nspcc-dev/neofs-api-go/v2/signature"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
"github.com/nspcc-dev/neofs-sdk-go/crypto/test"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test"
"github.com/stretchr/testify/require"
@ -86,7 +84,7 @@ func TestObjectIterate(t *testing.T) {
p, resp := testListReaderResponse(t)
var actual []oid.ID
resp.stream = &singleStreamResponder{key: p, idList: [][]oid.ID{ids}}
resp.stream = &singleStreamResponder{signer: p, idList: [][]oid.ID{ids}}
require.NoError(t, resp.Iterate(func(id oid.ID) bool {
actual = append(actual, id)
return len(actual) == 2
@ -109,27 +107,24 @@ func TestObjectIterate(t *testing.T) {
})
}
func testListReaderResponse(t *testing.T) (*ecdsa.PrivateKey, *ObjectListReader) {
p, err := keys.NewPrivateKey()
require.NoError(t, err)
return &p.PrivateKey, &ObjectListReader{
func testListReaderResponse(t *testing.T) (neofscrypto.Signer, *ObjectListReader) {
return test.RandomSigner(t), &ObjectListReader{
cancelCtxStream: func() {},
client: &Client{},
tail: nil,
}
}
func newSearchStream(key *ecdsa.PrivateKey, endError error, idList ...[]oid.ID) *singleStreamResponder {
func newSearchStream(signer neofscrypto.Signer, endError error, idList ...[]oid.ID) *singleStreamResponder {
return &singleStreamResponder{
key: key,
signer: signer,
endError: endError,
idList: idList,
}
}
type singleStreamResponder struct {
key *ecdsa.PrivateKey
signer neofscrypto.Signer
n int
endError error
idList [][]oid.ID
@ -140,7 +135,7 @@ func (s *singleStreamResponder) Read(resp *v2object.SearchResponse) error {
if s.endError != nil {
return s.endError
}
panic("unexpected call to `Read`")
return ErrUnexpectedReadCall
}
var body v2object.SearchResponseBody
@ -154,9 +149,9 @@ func (s *singleStreamResponder) Read(resp *v2object.SearchResponse) error {
}
resp.SetBody(&body)
err := signatureV2.SignServiceMessage(s.key, resp)
err := signServiceMessage(s.signer, resp)
if err != nil {
panic(fmt.Errorf("error: %w", err))
return err
}
s.n++

View file

@ -32,33 +32,23 @@ func (x *PrmAnnounceLocalTrust) SetValues(trusts []reputation.Trust) {
x.trusts = trusts
}
// ResAnnounceLocalTrust groups results of AnnounceLocalTrust operation.
type ResAnnounceLocalTrust struct {
statusRes
}
// AnnounceLocalTrust sends client's trust values to the NeoFS network participants.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// Any errors (local or remote, including returned status codes) are returned as Go errors,
// see [apistatus] package for NeoFS-specific error types.
//
// Immediately panics if parameters are set incorrectly (see PrmAnnounceLocalTrust docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
func (c *Client) AnnounceLocalTrust(ctx context.Context, prm PrmAnnounceLocalTrust) (*ResAnnounceLocalTrust, error) {
// Return errors:
// - [ErrZeroEpoch]
// - [ErrMissingTrusts]
func (c *Client) AnnounceLocalTrust(ctx context.Context, prm PrmAnnounceLocalTrust) error {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case prm.epoch == 0:
panic("zero epoch")
return ErrZeroEpoch
case len(prm.trusts) == 0:
panic("missing trusts")
return ErrMissingTrusts
}
// form request body
@ -82,23 +72,21 @@ func (c *Client) AnnounceLocalTrust(ctx context.Context, prm PrmAnnounceLocalTru
var (
cc contextCall
res ResAnnounceLocalTrust
)
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.AnnounceLocalTrust(&c.c, &req, client.WithContext(ctx))
}
// process call
if !cc.processCall() {
return nil, cc.err
return cc.err
}
return &res, nil
return nil
}
// PrmAnnounceIntermediateTrust groups parameters of AnnounceIntermediateTrust operation.
@ -132,34 +120,24 @@ func (x *PrmAnnounceIntermediateTrust) SetCurrentValue(trust reputation.PeerToPe
x.trustSet = true
}
// ResAnnounceIntermediateTrust groups results of AnnounceIntermediateTrust operation.
type ResAnnounceIntermediateTrust struct {
statusRes
}
// AnnounceIntermediateTrust sends global trust values calculated for the specified NeoFS network participants
// at some stage of client's calculation algorithm.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// Any errors (local or remote, including returned status codes) are returned as Go errors,
// see [apistatus] package for NeoFS-specific error types.
//
// Immediately panics if parameters are set incorrectly (see PrmAnnounceIntermediateTrust docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
func (c *Client) AnnounceIntermediateTrust(ctx context.Context, prm PrmAnnounceIntermediateTrust) (*ResAnnounceIntermediateTrust, error) {
// Return errors:
// - [ErrZeroEpoch]
// - [ErrMissingTrust]
func (c *Client) AnnounceIntermediateTrust(ctx context.Context, prm PrmAnnounceIntermediateTrust) error {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case prm.epoch == 0:
panic("zero epoch")
return ErrZeroEpoch
case !prm.trustSet:
panic("current trust value not set")
return ErrMissingTrust
}
var trust v2reputation.PeerToPeerTrust
@ -180,21 +158,19 @@ func (c *Client) AnnounceIntermediateTrust(ctx context.Context, prm PrmAnnounceI
var (
cc contextCall
res ResAnnounceIntermediateTrust
)
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.AnnounceIntermediateResult(&c.c, &req, client.WithContext(ctx))
}
// process call
if !cc.processCall() {
return nil, cc.err
return cc.err
}
return &res, nil
return nil
}

View file

@ -2,12 +2,12 @@ package client
import (
"context"
"crypto/ecdsa"
"fmt"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
"github.com/nspcc-dev/neofs-sdk-go/user"
)
@ -17,8 +17,7 @@ type PrmSessionCreate struct {
exp uint64
keySet bool
key ecdsa.PrivateKey
signer neofscrypto.Signer
}
// SetExp sets number of the last NepFS epoch in the lifetime of the session after which it will be expired.
@ -26,17 +25,14 @@ func (x *PrmSessionCreate) SetExp(exp uint64) {
x.exp = exp
}
// UseKey specifies private key to sign the requests and compute token owner.
// If key is not provided, then Client default key is used.
func (x *PrmSessionCreate) UseKey(key ecdsa.PrivateKey) {
x.keySet = true
x.key = key
// UseSigner specifies private signer to sign the requests and compute token owner.
// If signer is not provided, then Client default signer is used.
func (x *PrmSessionCreate) UseSigner(signer neofscrypto.Signer) {
x.signer = signer
}
// ResSessionCreate groups resulting values of SessionCreate operation.
type ResSessionCreate struct {
statusRes
id []byte
sessionKey []byte
@ -66,29 +62,27 @@ func (x ResSessionCreate) PublicKey() []byte {
// The session lifetime coincides with the server lifetime. Results can be written
// to session token which can be later attached to the requests.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveNeoFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
// Any errors (local or remote, including returned status codes) are returned as Go errors,
// see [apistatus] package for NeoFS-specific error types.
//
// Immediately panics if parameters are set incorrectly (see PrmSessionCreate docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
// Return errors:
// - [ErrMissingSigner]
func (c *Client) SessionCreate(ctx context.Context, prm PrmSessionCreate) (*ResSessionCreate, error) {
// check context
if ctx == nil {
panic(panicMsgMissingContext)
signer := prm.signer
if signer == nil {
signer = c.prm.signer
}
ownerKey := c.prm.key.PublicKey
if prm.keySet {
ownerKey = prm.key.PublicKey
if signer == nil {
return nil, ErrMissingSigner
}
var ownerID user.ID
user.IDFromKey(&ownerID, ownerKey)
if err := user.IDFromSigner(&ownerID, signer); err != nil {
return nil, fmt.Errorf("IDFromSigner: %w", err)
}
var ownerIDV2 refs.OwnerID
ownerID.WriteToV2(&ownerIDV2)
@ -111,21 +105,27 @@ func (c *Client) SessionCreate(ctx context.Context, prm PrmSessionCreate) (*ResS
)
c.initCallContext(&cc)
if prm.keySet {
cc.key = prm.key
}
cc.signer = signer
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.CreateSession(&c.c, &req, client.WithContext(ctx))
return c.server.createSession(&c.c, &req, client.WithContext(ctx))
}
cc.result = func(r responseV2) {
resp := r.(*v2session.CreateResponse)
body := resp.GetBody()
if len(body.GetID()) == 0 {
cc.err = newErrMissingResponseField("session id")
return
}
if len(body.GetSessionKey()) == 0 {
cc.err = newErrMissingResponseField("session key")
return
}
res.setID(body.GetID())
res.setSessionKey(body.GetSessionKey())
}

69
client/session_test.go Normal file
View file

@ -0,0 +1,69 @@
package client
import (
"context"
"testing"
v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"github.com/nspcc-dev/neofs-api-go/v2/session"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
"github.com/nspcc-dev/neofs-sdk-go/crypto/test"
"github.com/stretchr/testify/require"
)
type sessionAPIServer struct {
signer neofscrypto.Signer
setBody func(body *session.CreateResponseBody)
}
func (m sessionAPIServer) netMapSnapshot(context.Context, v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) {
return nil, nil
}
func (m sessionAPIServer) createSession(*client.Client, *session.CreateRequest, ...client.CallOption) (*session.CreateResponse, error) {
var body session.CreateResponseBody
m.setBody(&body)
var resp session.CreateResponse
resp.SetBody(&body)
if err := signServiceMessage(m.signer, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func TestClient_SessionCreate(t *testing.T) {
ctx := context.Background()
signer := test.RandomSignerRFC6979(t)
c := newClient(t, signer, nil)
var prmSessionCreate PrmSessionCreate
prmSessionCreate.UseSigner(signer)
prmSessionCreate.SetExp(1)
t.Run("missing session id", func(t *testing.T) {
c.setNeoFSAPIServer(&sessionAPIServer{signer: signer, setBody: func(body *session.CreateResponseBody) {
body.SetSessionKey([]byte{1})
}})
result, err := c.SessionCreate(ctx, prmSessionCreate)
require.Nil(t, result)
require.ErrorIs(t, err, ErrMissingResponseField)
require.Equal(t, "missing session id field in the response", err.Error())
})
t.Run("missing session key", func(t *testing.T) {
c.setNeoFSAPIServer(&sessionAPIServer{signer: signer, setBody: func(body *session.CreateResponseBody) {
body.SetID([]byte{1})
}})
result, err := c.SessionCreate(ctx, prmSessionCreate)
require.Nil(t, result)
require.ErrorIs(t, err, ErrMissingResponseField)
require.Equal(t, "missing session key field in the response", err.Error())
})
}

396
client/sign.go Normal file
View file

@ -0,0 +1,396 @@
package client
import (
"errors"
"fmt"
"github.com/nspcc-dev/neofs-api-go/v2/accounting"
"github.com/nspcc-dev/neofs-api-go/v2/container"
"github.com/nspcc-dev/neofs-api-go/v2/netmap"
"github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
"github.com/nspcc-dev/neofs-api-go/v2/reputation"
"github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/util/signature"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
)
type serviceRequest interface {
GetMetaHeader() *session.RequestMetaHeader
GetVerificationHeader() *session.RequestVerificationHeader
SetVerificationHeader(*session.RequestVerificationHeader)
}
type serviceResponse interface {
GetMetaHeader() *session.ResponseMetaHeader
GetVerificationHeader() *session.ResponseVerificationHeader
SetVerificationHeader(*session.ResponseVerificationHeader)
}
type stableMarshaler interface {
StableMarshal([]byte) []byte
StableSize() int
}
type stableMarshalerWrapper struct {
SM stableMarshaler
}
type metaHeader interface {
stableMarshaler
getOrigin() metaHeader
}
type verificationHeader interface {
stableMarshaler
GetBodySignature() *refs.Signature
SetBodySignature(*refs.Signature)
GetMetaSignature() *refs.Signature
SetMetaSignature(*refs.Signature)
GetOriginSignature() *refs.Signature
SetOriginSignature(*refs.Signature)
setOrigin(stableMarshaler)
getOrigin() verificationHeader
}
type requestMetaHeader struct {
*session.RequestMetaHeader
}
type responseMetaHeader struct {
*session.ResponseMetaHeader
}
type requestVerificationHeader struct {
*session.RequestVerificationHeader
}
type responseVerificationHeader struct {
*session.ResponseVerificationHeader
}
func (h *requestMetaHeader) getOrigin() metaHeader {
return &requestMetaHeader{
RequestMetaHeader: h.GetOrigin(),
}
}
func (h *responseMetaHeader) getOrigin() metaHeader {
return &responseMetaHeader{
ResponseMetaHeader: h.GetOrigin(),
}
}
func (h *requestVerificationHeader) getOrigin() verificationHeader {
if origin := h.GetOrigin(); origin != nil {
return &requestVerificationHeader{
RequestVerificationHeader: origin,
}
}
return nil
}
func (h *requestVerificationHeader) setOrigin(m stableMarshaler) {
if m != nil {
h.SetOrigin(m.(*session.RequestVerificationHeader))
}
}
func (r *responseVerificationHeader) getOrigin() verificationHeader {
if origin := r.GetOrigin(); origin != nil {
return &responseVerificationHeader{
ResponseVerificationHeader: origin,
}
}
return nil
}
func (r *responseVerificationHeader) setOrigin(m stableMarshaler) {
if m != nil {
r.SetOrigin(m.(*session.ResponseVerificationHeader))
}
}
func (s stableMarshalerWrapper) ReadSignedData(buf []byte) ([]byte, error) {
if s.SM != nil {
return s.SM.StableMarshal(buf), nil
}
return nil, nil
}
func (s stableMarshalerWrapper) SignedDataSize() int {
if s.SM != nil {
return s.SM.StableSize()
}
return 0
}
// signServiceMessage signing request or response messages which can be sent or received from neofs endpoint.
// Return errors:
// - [ErrSign]
func signServiceMessage(signer neofscrypto.Signer, msg interface{}) error {
var (
body, meta, verifyOrigin stableMarshaler
verifyHdr verificationHeader
verifyHdrSetter func(verificationHeader)
)
switch v := msg.(type) {
case nil:
return nil
case serviceRequest:
body = serviceMessageBody(v)
meta = v.GetMetaHeader()
verifyHdr = &requestVerificationHeader{new(session.RequestVerificationHeader)}
verifyHdrSetter = func(h verificationHeader) {
v.SetVerificationHeader(h.(*requestVerificationHeader).RequestVerificationHeader)
}
if h := v.GetVerificationHeader(); h != nil {
verifyOrigin = h
}
case serviceResponse:
body = serviceMessageBody(v)
meta = v.GetMetaHeader()
verifyHdr = &responseVerificationHeader{new(session.ResponseVerificationHeader)}
verifyHdrSetter = func(h verificationHeader) {
v.SetVerificationHeader(h.(*responseVerificationHeader).ResponseVerificationHeader)
}
if h := v.GetVerificationHeader(); h != nil {
verifyOrigin = h
}
default:
return NewSignError(fmt.Errorf("unsupported session message %T", v))
}
if verifyOrigin == nil {
// sign session message body
if err := signServiceMessagePart(signer, body, verifyHdr.SetBodySignature); err != nil {
return NewSignError(fmt.Errorf("body: %w", err))
}
}
// sign meta header
if err := signServiceMessagePart(signer, meta, verifyHdr.SetMetaSignature); err != nil {
return NewSignError(fmt.Errorf("meta header: %w", err))
}
// sign verification header origin
if err := signServiceMessagePart(signer, verifyOrigin, verifyHdr.SetOriginSignature); err != nil {
return NewSignError(fmt.Errorf("origin of verification header: %w", err))
}
// wrap origin verification header
verifyHdr.setOrigin(verifyOrigin)
// update matryoshka verification header
verifyHdrSetter(verifyHdr)
return nil
}
func signServiceMessagePart(signer neofscrypto.Signer, part stableMarshaler, sigWrite func(*refs.Signature)) error {
var sig neofscrypto.Signature
var sigv2 refs.Signature
m := &stableMarshalerWrapper{part}
data, err := m.ReadSignedData(nil)
if err != nil {
return fmt.Errorf("ReadSignedData %w", err)
}
if err = sig.Calculate(signer, data); err != nil {
return fmt.Errorf("calculate %w", err)
}
sig.WriteToV2(&sigv2)
sigWrite(&sigv2)
return nil
}
func verifyServiceMessage(msg interface{}) error {
var (
meta metaHeader
verify verificationHeader
)
switch v := msg.(type) {
case nil:
return nil
case serviceRequest:
meta = &requestMetaHeader{
RequestMetaHeader: v.GetMetaHeader(),
}
verify = &requestVerificationHeader{
RequestVerificationHeader: v.GetVerificationHeader(),
}
case serviceResponse:
meta = &responseMetaHeader{
ResponseMetaHeader: v.GetMetaHeader(),
}
verify = &responseVerificationHeader{
ResponseVerificationHeader: v.GetVerificationHeader(),
}
default:
return fmt.Errorf("unsupported session message %T", v)
}
body := serviceMessageBody(msg)
size := body.StableSize()
if sz := meta.StableSize(); sz > size {
size = sz
}
if sz := verify.StableSize(); sz > size {
size = sz
}
buf := make([]byte, 0, size)
return verifyMatryoshkaLevel(body, meta, verify, buf)
}
func verifyMatryoshkaLevel(body stableMarshaler, meta metaHeader, verify verificationHeader, buf []byte) error {
if err := verifyServiceMessagePart(meta, verify.GetMetaSignature, buf); err != nil {
return fmt.Errorf("could not verify meta header: %w", err)
}
origin := verify.getOrigin()
if err := verifyServiceMessagePart(origin, verify.GetOriginSignature, buf); err != nil {
return fmt.Errorf("could not verify origin of verification header: %w", err)
}
if origin == nil {
if err := verifyServiceMessagePart(body, verify.GetBodySignature, buf); err != nil {
return fmt.Errorf("could not verify body: %w", err)
}
return nil
}
if verify.GetBodySignature() != nil {
return errors.New("body signature at the matryoshka upper level")
}
return verifyMatryoshkaLevel(body, meta.getOrigin(), origin, buf)
}
func verifyServiceMessagePart(part stableMarshaler, sigRdr func() *refs.Signature, buf []byte) error {
return signature.VerifyDataWithSource(
&stableMarshalerWrapper{part},
sigRdr,
signature.WithBuffer(buf),
)
}
func serviceMessageBody(req any) stableMarshaler {
switch v := req.(type) {
default:
panic(fmt.Sprintf("unsupported session message %T", req))
/* Accounting */
case *accounting.BalanceRequest:
return v.GetBody()
case *accounting.BalanceResponse:
return v.GetBody()
/* Session */
case *session.CreateRequest:
return v.GetBody()
case *session.CreateResponse:
return v.GetBody()
/* Container */
case *container.PutRequest:
return v.GetBody()
case *container.PutResponse:
return v.GetBody()
case *container.DeleteRequest:
return v.GetBody()
case *container.DeleteResponse:
return v.GetBody()
case *container.GetRequest:
return v.GetBody()
case *container.GetResponse:
return v.GetBody()
case *container.ListRequest:
return v.GetBody()
case *container.ListResponse:
return v.GetBody()
case *container.SetExtendedACLRequest:
return v.GetBody()
case *container.SetExtendedACLResponse:
return v.GetBody()
case *container.GetExtendedACLRequest:
return v.GetBody()
case *container.GetExtendedACLResponse:
return v.GetBody()
case *container.AnnounceUsedSpaceRequest:
return v.GetBody()
case *container.AnnounceUsedSpaceResponse:
return v.GetBody()
/* Object */
case *object.PutRequest:
return v.GetBody()
case *object.PutResponse:
return v.GetBody()
case *object.GetRequest:
return v.GetBody()
case *object.GetResponse:
return v.GetBody()
case *object.HeadRequest:
return v.GetBody()
case *object.HeadResponse:
return v.GetBody()
case *object.SearchRequest:
return v.GetBody()
case *object.SearchResponse:
return v.GetBody()
case *object.DeleteRequest:
return v.GetBody()
case *object.DeleteResponse:
return v.GetBody()
case *object.GetRangeRequest:
return v.GetBody()
case *object.GetRangeResponse:
return v.GetBody()
case *object.GetRangeHashRequest:
return v.GetBody()
case *object.GetRangeHashResponse:
return v.GetBody()
/* Netmap */
case *netmap.LocalNodeInfoRequest:
return v.GetBody()
case *netmap.LocalNodeInfoResponse:
return v.GetBody()
case *netmap.NetworkInfoRequest:
return v.GetBody()
case *netmap.NetworkInfoResponse:
return v.GetBody()
case *netmap.SnapshotRequest:
return v.GetBody()
case *netmap.SnapshotResponse:
return v.GetBody()
/* Reputation */
case *reputation.AnnounceLocalTrustRequest:
return v.GetBody()
case *reputation.AnnounceLocalTrustResponse:
return v.GetBody()
case *reputation.AnnounceIntermediateResultRequest:
return v.GetBody()
case *reputation.AnnounceIntermediateResultResponse:
return v.GetBody()
}
}

247
client/sign_test.go Normal file
View file

@ -0,0 +1,247 @@
package client
import (
"crypto/rand"
"testing"
"github.com/nspcc-dev/neofs-api-go/v2/accounting"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
"github.com/nspcc-dev/neofs-api-go/v2/session"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
"github.com/nspcc-dev/neofs-sdk-go/crypto/test"
"github.com/nspcc-dev/neofs-sdk-go/user"
"github.com/stretchr/testify/require"
)
type testResponse interface {
SetMetaHeader(*session.ResponseMetaHeader)
GetMetaHeader() *session.ResponseMetaHeader
}
func testOwner(t *testing.T, owner *refs.OwnerID, req any) {
originalValue := owner.GetValue()
owner.SetValue([]byte{1, 2, 3})
// verification must fail
require.Error(t, verifyServiceMessage(req))
owner.SetValue(originalValue)
require.NoError(t, verifyServiceMessage(req))
}
func testRequestSign(t *testing.T, signer neofscrypto.Signer, meta *session.RequestMetaHeader, req request) {
require.Error(t, verifyServiceMessage(req))
// sign request
require.NoError(t, signServiceMessage(signer, req))
// verification must pass
require.NoError(t, verifyServiceMessage(req))
meta.SetOrigin(req.GetMetaHeader())
req.SetMetaHeader(meta)
// sign request
require.NoError(t, signServiceMessage(signer, req))
// verification must pass
require.NoError(t, verifyServiceMessage(req))
}
func testRequestMeta(t *testing.T, meta *session.RequestMetaHeader, req serviceRequest) {
// corrupt meta header
meta.SetTTL(meta.GetTTL() + 1)
// verification must fail
require.Error(t, verifyServiceMessage(req))
// restore meta header
meta.SetTTL(meta.GetTTL() - 1)
// corrupt origin verification header
req.GetVerificationHeader().SetOrigin(nil)
// verification must fail
require.Error(t, verifyServiceMessage(req))
}
func testResponseSign(t *testing.T, signer neofscrypto.Signer, meta *session.ResponseMetaHeader, resp testResponse) {
require.Error(t, verifyServiceMessage(resp))
// sign request
require.NoError(t, signServiceMessage(signer, resp))
// verification must pass
require.NoError(t, verifyServiceMessage(resp))
meta.SetOrigin(resp.GetMetaHeader())
resp.SetMetaHeader(meta)
// sign request
require.NoError(t, signServiceMessage(signer, resp))
// verification must pass
require.NoError(t, verifyServiceMessage(resp))
}
func testResponseMeta(t *testing.T, meta *session.ResponseMetaHeader, req serviceResponse) {
// corrupt meta header
meta.SetTTL(meta.GetTTL() + 1)
// verification must fail
require.Error(t, verifyServiceMessage(req))
// restore meta header
meta.SetTTL(meta.GetTTL() - 1)
// corrupt origin verification header
req.GetVerificationHeader().SetOrigin(nil)
// verification must fail
require.Error(t, verifyServiceMessage(req))
}
func TestEmptyMessage(t *testing.T) {
signer := test.RandomSignerRFC6979(t)
require.NoError(t, verifyServiceMessage(nil))
require.NoError(t, signServiceMessage(signer, nil))
}
func TestBalanceRequest(t *testing.T) {
signer := test.RandomSignerRFC6979(t)
var id user.ID
require.NoError(t, user.IDFromSigner(&id, signer))
var ownerID refs.OwnerID
id.WriteToV2(&ownerID)
body := accounting.BalanceRequestBody{}
body.SetOwnerID(&ownerID)
meta := &session.RequestMetaHeader{}
meta.SetTTL(1)
req := &accounting.BalanceRequest{}
req.SetBody(&body)
req.SetMetaHeader(meta)
// add level to meta header matryoshka
meta = &session.RequestMetaHeader{}
testRequestSign(t, signer, meta, req)
testOwner(t, &ownerID, req)
testRequestMeta(t, meta, req)
}
func TestBalanceResponse(t *testing.T) {
signer := test.RandomSignerRFC6979(t)
dec := new(accounting.Decimal)
dec.SetValue(100)
body := new(accounting.BalanceResponseBody)
body.SetBalance(dec)
meta := new(session.ResponseMetaHeader)
meta.SetTTL(1)
resp := new(accounting.BalanceResponse)
resp.SetBody(body)
resp.SetMetaHeader(meta)
// add level to meta header matryoshka
meta = new(session.ResponseMetaHeader)
testResponseSign(t, signer, meta, resp)
// corrupt body
dec.SetValue(dec.GetValue() + 1)
// verification must fail
require.Error(t, verifyServiceMessage(resp))
// restore body
dec.SetValue(dec.GetValue() - 1)
testResponseMeta(t, meta, resp)
}
func TestCreateRequest(t *testing.T) {
signer := test.RandomSignerRFC6979(t)
var id user.ID
require.NoError(t, user.IDFromSigner(&id, signer))
var ownerID refs.OwnerID
id.WriteToV2(&ownerID)
body := session.CreateRequestBody{}
body.SetOwnerID(&ownerID)
body.SetExpiration(100)
meta := &session.RequestMetaHeader{}
meta.SetTTL(1)
req := &session.CreateRequest{}
req.SetBody(&body)
req.SetMetaHeader(meta)
// add level to meta header matryoshka
meta = &session.RequestMetaHeader{}
testRequestSign(t, signer, meta, req)
testOwner(t, &ownerID, req)
// corrupt body
body.SetExpiration(body.GetExpiration() + 1)
// verification must fail
require.Error(t, verifyServiceMessage(req))
// restore body
body.SetExpiration(body.GetExpiration() - 1)
testRequestMeta(t, meta, req)
}
func TestCreateResponse(t *testing.T) {
signer := test.RandomSignerRFC6979(t)
id := make([]byte, 8)
_, err := rand.Read(id)
require.NoError(t, err)
sessionKey := make([]byte, 8)
_, err = rand.Read(sessionKey)
require.NoError(t, err)
body := session.CreateResponseBody{}
body.SetID(id)
body.SetSessionKey(sessionKey)
meta := &session.ResponseMetaHeader{}
meta.SetTTL(1)
req := &session.CreateResponse{}
req.SetBody(&body)
req.SetMetaHeader(meta)
// add level to meta header matryoshka
meta = &session.ResponseMetaHeader{}
testResponseSign(t, signer, meta, req)
// corrupt body
body.SetID([]byte{1})
// verification must fail
require.Error(t, verifyServiceMessage(req))
// restore body
body.SetID(id)
// corrupt body
body.SetSessionKey([]byte{1})
// verification must fail
require.Error(t, verifyServiceMessage(req))
// restore body
body.SetSessionKey(id)
testResponseMeta(t, meta, req)
}

View file

@ -2,12 +2,32 @@ package apistatus
import (
"encoding/binary"
"errors"
"github.com/nspcc-dev/neofs-api-go/v2/status"
)
// Error describes common error which is a grouping type for any [apistatus] errors. Any [apistatus] error may be checked
// explicitly via it's type of just check the group via errors.Is(err, [apistatus.Error]).
var Error = errors.New("api error")
var (
// ErrServerInternal is an instance of ServerInternal error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
ErrServerInternal ServerInternal
// ErrWrongMagicNumber is an instance of WrongMagicNumber error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
ErrWrongMagicNumber WrongMagicNumber
// ErrSignatureVerification is an instance of SignatureVerification error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
ErrSignatureVerification SignatureVerification
// ErrNodeUnderMaintenance is an instance of NodeUnderMaintenance error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
ErrNodeUnderMaintenance NodeUnderMaintenance
)
// ServerInternal describes failure statuses related to internal server errors.
// Instances provide Status and StatusV2 interfaces.
// Instances provide [StatusV2] and error interfaces.
//
// The status is purely informative, the client should not go into details of the error except for debugging needs.
type ServerInternal struct {
@ -21,18 +41,28 @@ func (x ServerInternal) Error() string {
)
}
// implements local interface defined in FromStatusV2 func.
// Is implements interface for correct checking current error type with [errors.Is].
func (x ServerInternal) Is(target error) bool {
switch target.(type) {
default:
return errors.Is(Error, target)
case ServerInternal, *ServerInternal:
return true
}
}
// implements local interface defined in [ErrorFromV2] func.
func (x *ServerInternal) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// ErrorToV2 implements [StatusV2] interface method.
// If the value was returned by [ErrorFromV2], returns the source message.
// Otherwise, returns message with
// - code: INTERNAL;
// - string message: empty;
// - details: empty.
func (x ServerInternal) ToStatusV2() *status.Status {
func (x ServerInternal) ErrorToV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(status.Internal, status.GlobalizeCommonFail))
return &x.v2
}
@ -57,7 +87,7 @@ func WriteInternalServerErr(x *ServerInternal, err error) {
}
// WrongMagicNumber describes failure status related to incorrect network magic.
// Instances provide Status and StatusV2 interfaces.
// Instances provide [StatusV2] and error interfaces.
type WrongMagicNumber struct {
v2 status.Status
}
@ -69,18 +99,28 @@ func (x WrongMagicNumber) Error() string {
)
}
// implements local interface defined in FromStatusV2 func.
// Is implements interface for correct checking current error type with [errors.Is].
func (x WrongMagicNumber) Is(target error) bool {
switch target.(type) {
default:
return errors.Is(Error, target)
case WrongMagicNumber, *WrongMagicNumber:
return true
}
}
// implements local interface defined in [ErrorFromV2] func.
func (x *WrongMagicNumber) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// ErrorToV2 implements [StatusV2] interface method.
// If the value was returned by [ErrorFromV2], returns the source message.
// Otherwise, returns message with
// - code: WRONG_MAGIC_NUMBER;
// - string message: empty;
// - details: empty.
func (x WrongMagicNumber) ToStatusV2() *status.Status {
func (x WrongMagicNumber) ErrorToV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(status.WrongMagicNumber, status.GlobalizeCommonFail))
return &x.v2
}
@ -125,35 +165,52 @@ func (x WrongMagicNumber) CorrectMagic() (magic uint64, ok int8) {
}
// SignatureVerification describes failure status related to signature verification.
// Instances provide Status and StatusV2 interfaces.
// Instances provide [StatusV2] and error interfaces.
type SignatureVerification struct {
v2 status.Status
}
const defaultSignatureVerificationMsg = "signature verification failed"
func (x SignatureVerification) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultSignatureVerificationMsg
}
return errMessageStatusV2(
globalizeCodeV2(status.SignatureVerificationFail, status.GlobalizeCommonFail),
x.v2.Message(),
msg,
)
}
// implements local interface defined in FromStatusV2 func.
// Is implements interface for correct checking current error type with [errors.Is].
func (x SignatureVerification) Is(target error) bool {
switch target.(type) {
default:
return errors.Is(Error, target)
case SignatureVerification, *SignatureVerification:
return true
}
}
// implements local interface defined in [ErrorFromV2] func.
func (x *SignatureVerification) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// ErrorToV2 implements [StatusV2] interface method.
// If the value was returned by [ErrorFromV2], returns the source message.
// Otherwise, returns message with
// - code: SIGNATURE_VERIFICATION_FAIL;
// - string message: written message via SetMessage or
// - string message: written message via [SignatureVerification.SetMessage] or
// "signature verification failed" as a default message;
// - details: empty.
func (x SignatureVerification) ToStatusV2() *status.Status {
func (x SignatureVerification) ErrorToV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(status.SignatureVerificationFail, status.GlobalizeCommonFail))
if x.v2.Message() == "" {
x.v2.SetMessage("signature verification failed")
x.v2.SetMessage(defaultSignatureVerificationMsg)
}
return &x.v2
@ -176,16 +233,18 @@ func (x SignatureVerification) Message() string {
}
// NodeUnderMaintenance describes failure status for nodes being under maintenance.
// Instances provide Status and StatusV2 interfaces.
// Instances provide [StatusV2] and error interfaces.
type NodeUnderMaintenance struct {
v2 status.Status
}
const defaultNodeUnderMaintenanceMsg = "node is under maintenance"
// Error implements the error interface.
func (x NodeUnderMaintenance) Error() string {
msg := x.Message()
if msg == "" {
msg = "node is under maintenance"
msg = defaultNodeUnderMaintenanceMsg
}
return errMessageStatusV2(
@ -194,18 +253,33 @@ func (x NodeUnderMaintenance) Error() string {
)
}
// Is implements interface for correct checking current error type with [errors.Is].
func (x NodeUnderMaintenance) Is(target error) bool {
switch target.(type) {
default:
return errors.Is(Error, target)
case NodeUnderMaintenance, *NodeUnderMaintenance:
return true
}
}
func (x *NodeUnderMaintenance) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// ErrorToV2 implements [StatusV2] interface method.
// If the value was returned by [ErrorFromV2], returns the source message.
// Otherwise, returns message with
// - code: NODE_UNDER_MAINTENANCE;
// - string message: written message via SetMessage;
// - string message: written message via [NodeUnderMaintenance.SetMessage] or
// "node is under maintenance" as a default message;
// - details: empty.
func (x NodeUnderMaintenance) ToStatusV2() *status.Status {
func (x NodeUnderMaintenance) ErrorToV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(status.NodeUnderMaintenance, status.GlobalizeCommonFail))
if x.v2.Message() == "" {
x.v2.SetMessage(defaultNodeUnderMaintenanceMsg)
}
return &x.v2
}

View file

@ -14,14 +14,14 @@ func TestServerInternal_Message(t *testing.T) {
var st apistatus.ServerInternal
res := st.Message()
resv2 := apistatus.ToStatusV2(st).Message()
resv2 := apistatus.ErrorToV2(st).Message()
require.Empty(t, res)
require.Empty(t, resv2)
st.SetMessage(msg)
res = st.Message()
resv2 = apistatus.ToStatusV2(st).Message()
resv2 = apistatus.ErrorToV2(st).Message()
require.Equal(t, msg, res)
require.Equal(t, msg, resv2)
}
@ -42,7 +42,7 @@ func TestWrongMagicNumber_CorrectMagic(t *testing.T) {
require.EqualValues(t, 1, ok)
// corrupt the value
apistatus.ToStatusV2(st).IterateDetails(func(d *status.Detail) bool {
apistatus.ErrorToV2(st).IterateDetails(func(d *status.Detail) bool {
d.SetValue([]byte{1, 2, 3}) // any slice with len != 8
return true
})
@ -64,7 +64,7 @@ func TestSignatureVerification(t *testing.T) {
st.SetMessage(msg)
stV2 := st.ToStatusV2()
stV2 := st.ErrorToV2()
require.Equal(t, msg, st.Message())
require.Equal(t, msg, stV2.Message())
@ -73,7 +73,7 @@ func TestSignatureVerification(t *testing.T) {
t.Run("empty to V2", func(t *testing.T) {
var st apistatus.SignatureVerification
stV2 := st.ToStatusV2()
stV2 := st.ErrorToV2()
require.Equal(t, "signature verification failed", stV2.Message())
})
@ -84,7 +84,7 @@ func TestSignatureVerification(t *testing.T) {
st.SetMessage(msg)
stV2 := st.ToStatusV2()
stV2 := st.ErrorToV2()
require.Equal(t, msg, stV2.Message())
})
@ -103,7 +103,7 @@ func TestNodeUnderMaintenance(t *testing.T) {
st.SetMessage(msg)
stV2 := st.ToStatusV2()
stV2 := st.ErrorToV2()
require.Equal(t, msg, st.Message())
require.Equal(t, msg, stV2.Message())
@ -112,7 +112,7 @@ func TestNodeUnderMaintenance(t *testing.T) {
t.Run("empty to V2", func(t *testing.T) {
var st apistatus.NodeUnderMaintenance
stV2 := st.ToStatusV2()
stV2 := st.ErrorToV2()
require.Empty(t, "", stV2.Message())
})
@ -123,7 +123,7 @@ func TestNodeUnderMaintenance(t *testing.T) {
st.SetMessage(msg)
stV2 := st.ToStatusV2()
stV2 := st.ErrorToV2()
require.Equal(t, msg, stV2.Message())
})

View file

@ -1,67 +1,112 @@
package apistatus
import (
"errors"
"github.com/nspcc-dev/neofs-api-go/v2/container"
"github.com/nspcc-dev/neofs-api-go/v2/status"
)
var (
// ErrEACLNotFound is an instance of EACLNotFound error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
ErrEACLNotFound EACLNotFound
// ErrContainerNotFound is an instance of ContainerNotFound error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
ErrContainerNotFound ContainerNotFound
)
// ContainerNotFound describes status of the failure because of the missing container.
// Instances provide Status and StatusV2 interfaces.
// Instances provide [StatusV2] and error interfaces.
type ContainerNotFound struct {
v2 status.Status
}
const defaultContainerNotFoundMsg = "container not found"
func (x ContainerNotFound) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultContainerNotFoundMsg
}
return errMessageStatusV2(
globalizeCodeV2(container.StatusNotFound, container.GlobalizeFail),
x.v2.Message(),
msg,
)
}
// implements local interface defined in FromStatusV2 func.
// Is implements interface for correct checking current error type with [errors.Is].
func (x ContainerNotFound) Is(target error) bool {
switch target.(type) {
default:
return errors.Is(Error, target)
case ContainerNotFound, *ContainerNotFound:
return true
}
}
// implements local interface defined in [ErrorFromV2] func.
func (x *ContainerNotFound) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// ErrorToV2 implements [StatusV2] interface method.
// If the value was returned by [ErrorFromV2], returns the source message.
// Otherwise, returns message with
// - code: CONTAINER_NOT_FOUND;
// - string message: "container not found";
// - details: empty.
func (x ContainerNotFound) ToStatusV2() *status.Status {
func (x ContainerNotFound) ErrorToV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(container.StatusNotFound, container.GlobalizeFail))
x.v2.SetMessage("container not found")
x.v2.SetMessage(defaultContainerNotFoundMsg)
return &x.v2
}
// EACLNotFound describes status of the failure because of the missing eACL
// table.
// Instances provide Status and StatusV2 interfaces.
// Instances provide [StatusV2] and error interfaces.
type EACLNotFound struct {
v2 status.Status
}
const defaultEACLNotFoundMsg = "eACL not found"
func (x EACLNotFound) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultEACLNotFoundMsg
}
return errMessageStatusV2(
globalizeCodeV2(container.StatusEACLNotFound, container.GlobalizeFail),
x.v2.Message(),
msg,
)
}
// implements local interface defined in FromStatusV2 func.
// Is implements interface for correct checking current error type with [errors.Is].
func (x EACLNotFound) Is(target error) bool {
switch target.(type) {
default:
return errors.Is(Error, target)
case EACLNotFound, *EACLNotFound:
return true
}
}
// implements local interface defined in [ErrorFromV2] func.
func (x *EACLNotFound) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// ErrorToV2 implements [StatusV2] interface method.
// If the value was returned by [ErrorFromV2], returns the source message.
// Otherwise, returns message with
// - code: EACL_NOT_FOUND;
// - string message: "eACL not found";
// - details: empty.
func (x EACLNotFound) ToStatusV2() *status.Status {
func (x EACLNotFound) ErrorToV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(container.StatusEACLNotFound, container.GlobalizeFail))
x.v2.SetMessage("eACL not found")
x.v2.SetMessage(defaultEACLNotFoundMsg)
return &x.v2
}

View file

@ -0,0 +1,93 @@
package apistatus
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestErrors(t *testing.T) {
for _, tc := range []struct {
errs []error
errVariable error
}{
{
errs: []error{ServerInternal{}, new(ServerInternal)},
errVariable: ErrServerInternal,
},
{
errs: []error{WrongMagicNumber{}, new(WrongMagicNumber)},
errVariable: ErrWrongMagicNumber,
},
{
errs: []error{SignatureVerification{}, new(SignatureVerification)},
errVariable: ErrSignatureVerification,
},
{
errs: []error{NodeUnderMaintenance{}, new(NodeUnderMaintenance)},
errVariable: ErrNodeUnderMaintenance,
},
{
errs: []error{ObjectLocked{}, new(ObjectLocked)},
errVariable: ErrObjectLocked,
},
{
errs: []error{LockNonRegularObject{}, new(LockNonRegularObject)},
errVariable: ErrLockNonRegularObject,
},
{
errs: []error{ObjectAccessDenied{}, new(ObjectAccessDenied)},
errVariable: ErrObjectAccessDenied,
},
{
errs: []error{ObjectNotFound{}, new(ObjectNotFound)},
errVariable: ErrObjectNotFound,
},
{
errs: []error{ObjectAlreadyRemoved{}, new(ObjectAlreadyRemoved)},
errVariable: ErrObjectAlreadyRemoved,
},
{
errs: []error{ObjectOutOfRange{}, new(ObjectOutOfRange)},
errVariable: ErrObjectOutOfRange,
},
{
errs: []error{ContainerNotFound{}, new(ContainerNotFound)},
errVariable: ErrContainerNotFound,
},
{
errs: []error{EACLNotFound{}, new(EACLNotFound)},
errVariable: ErrEACLNotFound,
},
{
errs: []error{SessionTokenExpired{}, new(SessionTokenExpired)},
errVariable: ErrSessionTokenExpired,
},
{
errs: []error{SessionTokenNotFound{}, new(SessionTokenNotFound)},
errVariable: ErrSessionTokenNotFound,
},
{
errs: []error{UnrecognizedStatusV2{}, new(UnrecognizedStatusV2)},
errVariable: ErrUnrecognizedStatusV2,
},
} {
require.NotEmpty(t, tc.errs)
require.NotNil(t, tc.errVariable)
for i := range tc.errs {
require.ErrorIs(t, tc.errs[i], tc.errVariable)
wrapped := fmt.Errorf("some message %w", tc.errs[i])
require.ErrorIs(t, wrapped, tc.errVariable)
wrappedTwice := fmt.Errorf("another message %w", wrapped)
require.ErrorIs(t, wrappedTwice, tc.errVariable)
}
}
}

View file

@ -1,97 +1,171 @@
package apistatus
import (
"errors"
"github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-api-go/v2/status"
)
var (
// ErrObjectLocked is an instance of ObjectLocked error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
ErrObjectLocked ObjectLocked
// ErrObjectAlreadyRemoved is an instance of ObjectAlreadyRemoved error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
ErrObjectAlreadyRemoved ObjectAlreadyRemoved
// ErrLockNonRegularObject is an instance of LockNonRegularObject error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
ErrLockNonRegularObject LockNonRegularObject
// ErrObjectAccessDenied is an instance of ObjectAccessDenied error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
ErrObjectAccessDenied ObjectAccessDenied
// ErrObjectNotFound is an instance of ObjectNotFound error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
ErrObjectNotFound ObjectNotFound
// ErrObjectOutOfRange is an instance of ObjectOutOfRange error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
ErrObjectOutOfRange ObjectOutOfRange
)
// ObjectLocked describes status of the failure because of the locked object.
// Instances provide Status and StatusV2 interfaces.
// Instances provide [StatusV2] and error interfaces.
type ObjectLocked struct {
v2 status.Status
}
const defaultObjectLockedMsg = "object is locked"
func (x ObjectLocked) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultObjectLockedMsg
}
return errMessageStatusV2(
globalizeCodeV2(object.StatusLocked, object.GlobalizeFail),
x.v2.Message(),
msg,
)
}
// implements local interface defined in FromStatusV2 func.
// Is implements interface for correct checking current error type with [errors.Is].
func (x ObjectLocked) Is(target error) bool {
switch target.(type) {
default:
return errors.Is(Error, target)
case ObjectLocked, *ObjectLocked:
return true
}
}
// implements local interface defined in [ErrorFromV2] func.
func (x *ObjectLocked) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// ErrorToV2 implements [StatusV2] interface method.
// If the value was returned by [ErrorFromV2], returns the source message.
// Otherwise, returns message with
// - code: LOCKED;
// - string message: "object is locked";
// - details: empty.
func (x ObjectLocked) ToStatusV2() *status.Status {
func (x ObjectLocked) ErrorToV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusLocked, object.GlobalizeFail))
x.v2.SetMessage("object is locked")
x.v2.SetMessage(defaultObjectLockedMsg)
return &x.v2
}
// LockNonRegularObject describes status returned on locking the non-regular object.
// Instances provide Status and StatusV2 interfaces.
// Instances provide [StatusV2] and error interfaces.
type LockNonRegularObject struct {
v2 status.Status
}
const defaultLockNonRegularObjectMsg = "locking non-regular object is forbidden"
func (x LockNonRegularObject) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultLockNonRegularObjectMsg
}
return errMessageStatusV2(
globalizeCodeV2(object.StatusLockNonRegularObject, object.GlobalizeFail),
x.v2.Message(),
msg,
)
}
// implements local interface defined in FromStatusV2 func.
// Is implements interface for correct checking current error type with [errors.Is].
func (x LockNonRegularObject) Is(target error) bool {
switch target.(type) {
default:
return errors.Is(Error, target)
case LockNonRegularObject, *LockNonRegularObject:
return true
}
}
// implements local interface defined in [ErrorFromV2] func.
func (x *LockNonRegularObject) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// ErrorToV2 implements [StatusV2] interface method.
// If the value was returned by [ErrorFromV2], returns the source message.
// Otherwise, returns message with
// - code: LOCK_NON_REGULAR_OBJECT;
// - string message: "locking non-regular object is forbidden";
// - details: empty.
func (x LockNonRegularObject) ToStatusV2() *status.Status {
func (x LockNonRegularObject) ErrorToV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusLockNonRegularObject, object.GlobalizeFail))
x.v2.SetMessage("locking non-regular object is forbidden")
x.v2.SetMessage(defaultLockNonRegularObjectMsg)
return &x.v2
}
// ObjectAccessDenied describes status of the failure because of the access control violation.
// Instances provide Status and StatusV2 interfaces.
// Instances provide [StatusV2] and error interfaces.
type ObjectAccessDenied struct {
v2 status.Status
}
const defaultObjectAccessDeniedMsg = "access to object operation denied"
func (x ObjectAccessDenied) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultObjectAccessDeniedMsg
}
return errMessageStatusV2(
globalizeCodeV2(object.StatusAccessDenied, object.GlobalizeFail),
x.v2.Message(),
msg,
)
}
// implements local interface defined in FromStatusV2 func.
// Is implements interface for correct checking current error type with [errors.Is].
func (x ObjectAccessDenied) Is(target error) bool {
switch target.(type) {
default:
return errors.Is(Error, target)
case ObjectAccessDenied, *ObjectAccessDenied:
return true
}
}
// implements local interface defined in [ErrorFromV2] func.
func (x *ObjectAccessDenied) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// ErrorToV2 implements [StatusV2] interface method.
// If the value was returned by [ErrorFromV2], returns the source message.
// Otherwise, returns message with
// - code: ACCESS_DENIED;
// - string message: "access to object operation denied";
// - details: empty.
func (x ObjectAccessDenied) ToStatusV2() *status.Status {
func (x ObjectAccessDenied) ErrorToV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusAccessDenied, object.GlobalizeFail))
x.v2.SetMessage("access to object operation denied")
x.v2.SetMessage(defaultObjectAccessDeniedMsg)
return &x.v2
}
@ -107,32 +181,49 @@ func (x ObjectAccessDenied) Reason() string {
}
// ObjectNotFound describes status of the failure because of the missing object.
// Instances provide Status and StatusV2 interfaces.
// Instances provide [StatusV2] and error interfaces.
type ObjectNotFound struct {
v2 status.Status
}
const defaultObjectNotFoundMsg = "object not found"
func (x ObjectNotFound) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultObjectNotFoundMsg
}
return errMessageStatusV2(
globalizeCodeV2(object.StatusNotFound, object.GlobalizeFail),
x.v2.Message(),
msg,
)
}
// implements local interface defined in FromStatusV2 func.
// Is implements interface for correct checking current error type with [errors.Is].
func (x ObjectNotFound) Is(target error) bool {
switch target.(type) {
default:
return errors.Is(Error, target)
case ObjectNotFound, *ObjectNotFound:
return true
}
}
// implements local interface defined in [ErrorFromV2] func.
func (x *ObjectNotFound) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// ErrorToV2 implements [StatusV2] interface method.
// If the value was returned by [ErrorFromV2], returns the source message.
// Otherwise, returns message with
// - code: OBJECT_NOT_FOUND;
// - string message: "object not found";
// - details: empty.
func (x ObjectNotFound) ToStatusV2() *status.Status {
func (x ObjectNotFound) ErrorToV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusNotFound, object.GlobalizeFail))
x.v2.SetMessage("object not found")
x.v2.SetMessage(defaultObjectNotFoundMsg)
return &x.v2
}
@ -142,57 +233,91 @@ type ObjectAlreadyRemoved struct {
v2 status.Status
}
const defaultObjectAlreadyRemovedMsg = "object already removed"
func (x ObjectAlreadyRemoved) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultObjectAlreadyRemovedMsg
}
return errMessageStatusV2(
globalizeCodeV2(object.StatusAlreadyRemoved, object.GlobalizeFail),
x.v2.Message(),
msg,
)
}
// implements local interface defined in FromStatusV2 func.
// Is implements interface for correct checking current error type with [errors.Is].
func (x ObjectAlreadyRemoved) Is(target error) bool {
switch target.(type) {
default:
return errors.Is(Error, target)
case ObjectAlreadyRemoved, *ObjectAlreadyRemoved:
return true
}
}
// implements local interface defined in [ErrorFromV2] func.
func (x *ObjectAlreadyRemoved) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// ErrorToV2 implements [StatusV2] interface method.
// If the value was returned by [ErrorFromV2], returns the source message.
// Otherwise, returns message with
// - code: OBJECT_ALREADY_REMOVED;
// - string message: "object already removed";
// - details: empty.
func (x ObjectAlreadyRemoved) ToStatusV2() *status.Status {
func (x ObjectAlreadyRemoved) ErrorToV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusAlreadyRemoved, object.GlobalizeFail))
x.v2.SetMessage("object already removed")
x.v2.SetMessage(defaultObjectAlreadyRemovedMsg)
return &x.v2
}
// ObjectOutOfRange describes status of the failure because of the incorrect
// provided object ranges.
// Instances provide Status and StatusV2 interfaces.
// Instances provide [StatusV2] and error interfaces.
type ObjectOutOfRange struct {
v2 status.Status
}
const defaultObjectOutOfRangeMsg = "out of range"
func (x ObjectOutOfRange) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultObjectOutOfRangeMsg
}
return errMessageStatusV2(
globalizeCodeV2(object.StatusOutOfRange, object.GlobalizeFail),
x.v2.Message(),
msg,
)
}
// implements local interface defined in FromStatusV2 func.
// Is implements interface for correct checking current error type with [errors.Is].
func (x ObjectOutOfRange) Is(target error) bool {
switch target.(type) {
default:
return errors.Is(Error, target)
case ObjectOutOfRange, *ObjectOutOfRange:
return true
}
}
// implements local interface defined in [ErrorFromV2] func.
func (x *ObjectOutOfRange) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// ErrorToV2 implements [StatusV2] interface method.
// If the value was returned by [ErrorFromV2], returns the source message.
// Otherwise, returns message with
// - code: OUT_OF_RANGE;
// - string message: "out of range";
// - details: empty.
func (x ObjectOutOfRange) ToStatusV2() *status.Status {
func (x ObjectOutOfRange) ErrorToV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusOutOfRange, object.GlobalizeFail))
x.v2.SetMessage("out of range")
x.v2.SetMessage(defaultObjectOutOfRangeMsg)
return &x.v2
}

View file

@ -14,13 +14,13 @@ func TestObjectAccessDenied_WriteReason(t *testing.T) {
res := st.Reason()
require.Empty(t, res)
detailNum := apistatus.ToStatusV2(st).NumberOfDetails()
detailNum := apistatus.ErrorToV2(st).NumberOfDetails()
require.Zero(t, detailNum)
st.WriteReason(reason)
res = st.Reason()
require.Equal(t, reason, res)
detailNum = apistatus.ToStatusV2(st).NumberOfDetails()
detailNum = apistatus.ErrorToV2(st).NumberOfDetails()
require.EqualValues(t, 1, detailNum)
}

View file

@ -1,66 +1,111 @@
package apistatus
import (
"errors"
"github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/status"
)
var (
// ErrSessionTokenNotFound is an instance of SessionTokenNotFound error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
ErrSessionTokenNotFound SessionTokenNotFound
// ErrSessionTokenExpired is an instance of SessionTokenExpired error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
ErrSessionTokenExpired SessionTokenExpired
)
// SessionTokenNotFound describes status of the failure because of the missing session token.
// Instances provide Status and StatusV2 interfaces.
// Instances provide [StatusV2] and error interfaces.
type SessionTokenNotFound struct {
v2 status.Status
}
const defaultSessionTokenNotFoundMsg = "session token not found"
func (x SessionTokenNotFound) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultSessionTokenNotFoundMsg
}
return errMessageStatusV2(
globalizeCodeV2(session.StatusTokenNotFound, session.GlobalizeFail),
x.v2.Message(),
msg,
)
}
// implements local interface defined in FromStatusV2 func.
// Is implements interface for correct checking current error type with [errors.Is].
func (x SessionTokenNotFound) Is(target error) bool {
switch target.(type) {
default:
return errors.Is(Error, target)
case SessionTokenNotFound, *SessionTokenNotFound:
return true
}
}
// implements local interface defined in [ErrorFromV2] func.
func (x *SessionTokenNotFound) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// ErrorToV2 implements [StatusV2] interface method.
// If the value was returned by [ErrorFromV2], returns the source message.
// Otherwise, returns message with
// - code: TOKEN_NOT_FOUND;
// - string message: "session token not found";
// - details: empty.
func (x SessionTokenNotFound) ToStatusV2() *status.Status {
func (x SessionTokenNotFound) ErrorToV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(session.StatusTokenNotFound, session.GlobalizeFail))
x.v2.SetMessage("session token not found")
x.v2.SetMessage(defaultSessionTokenNotFoundMsg)
return &x.v2
}
// SessionTokenExpired describes status of the failure because of the expired session token.
// Instances provide Status and StatusV2 interfaces.
// Instances provide [StatusV2] and error interfaces.
type SessionTokenExpired struct {
v2 status.Status
}
const defaultSessionTokenExpiredMsg = "expired session token"
func (x SessionTokenExpired) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultSessionTokenExpiredMsg
}
return errMessageStatusV2(
globalizeCodeV2(session.StatusTokenExpired, session.GlobalizeFail),
x.v2.Message(),
msg,
)
}
// implements local interface defined in FromStatusV2 func.
// Is implements interface for correct checking current error type with [errors.Is].
func (x SessionTokenExpired) Is(target error) bool {
switch target.(type) {
default:
return errors.Is(Error, target)
case SessionTokenExpired, *SessionTokenExpired:
return true
}
}
// implements local interface defined in [ErrorFromV2] func.
func (x *SessionTokenExpired) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// ErrorToV2 implements [StatusV2] interface method.
// If the value was returned by [ErrorFromV2], returns the source message.
// Otherwise, returns message with
// - code: TOKEN_EXPIRED;
// - string message: "expired session token";
// - details: empty.
func (x SessionTokenExpired) ToStatusV2() *status.Status {
func (x SessionTokenExpired) ErrorToV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(session.StatusTokenExpired, session.GlobalizeFail))
x.v2.SetMessage("expired session token")
x.v2.SetMessage(defaultSessionTokenExpiredMsg)
return &x.v2
}

View file

@ -1,44 +0,0 @@
package apistatus
// Status defines a variety of NeoFS API status returns.
//
// All statuses are split into two disjoint subsets: successful and failed, and:
// - statuses that implement the build-in error interface are considered failed statuses;
// - all other value types are considered successes (nil is a default success).
//
// In Go code type of success can be determined by a type switch, failure - by a switch with errors.As calls.
// Nil should be considered as a success, and default switch section - as an unrecognized Status.
//
// To convert statuses into errors and vice versa, use functions ErrToStatus and ErrFromStatus, respectively.
// ErrFromStatus function returns nil for successful statuses. However, to simplify the check of statuses for success,
// IsSuccessful function should be used (try to avoid nil comparison).
// It should be noted that using direct typecasting is not a compatible approach.
//
// To transport statuses using the NeoFS API V2 protocol, see StatusV2 interface and FromStatusV2 and ToStatusV2 functions.
type Status interface{}
// ErrFromStatus converts Status instance to error if it is failed. Returns nil on successful Status.
//
// Note: direct assignment may not be compatibility-safe.
func ErrFromStatus(st Status) error {
if err, ok := st.(error); ok {
return err
}
return nil
}
// ErrToStatus converts the error instance to Status instance.
//
// Note: direct assignment may not be compatibility-safe.
func ErrToStatus(err error) Status {
return err
}
// IsSuccessful checks if status is successful.
//
// Note: direct cast may not be compatibility-safe.
func IsSuccessful(st Status) bool {
_, ok := st.(error)
return !ok
}

View file

@ -1,35 +0,0 @@
package apistatus_test
import (
"errors"
"testing"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"github.com/stretchr/testify/require"
)
func TestErrors(t *testing.T) {
t.Run("error source", func(t *testing.T) {
err := errors.New("some error")
st := apistatus.ErrToStatus(err)
success := apistatus.IsSuccessful(st)
require.False(t, success)
res := apistatus.ErrFromStatus(st)
require.Equal(t, err, res)
})
t.Run("non-error source", func(t *testing.T) {
var st apistatus.Status = "any non-error type"
success := apistatus.IsSuccessful(st)
require.True(t, success)
res := apistatus.ErrFromStatus(st)
require.Nil(t, res)
})
}

View file

@ -1,32 +0,0 @@
package apistatus
import (
"github.com/nspcc-dev/neofs-api-go/v2/status"
)
// SuccessDefaultV2 represents Status instance of default success. Implements StatusV2.
type SuccessDefaultV2 struct {
isNil bool
v2 *status.Status
}
// implements local interface defined in FromStatusV2 func.
func (x *SuccessDefaultV2) fromStatusV2(st *status.Status) {
x.isNil = st == nil
x.v2 = st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with
// - code: OK;
// - string message: empty;
// - details: empty.
func (x SuccessDefaultV2) ToStatusV2() *status.Status {
if x.isNil || x.v2 != nil {
return x.v2
}
return newStatusV2WithLocalCode(status.OK, status.GlobalizeSuccess)
}

View file

@ -4,15 +4,31 @@ import (
"github.com/nspcc-dev/neofs-api-go/v2/status"
)
type unrecognizedStatusV2 struct {
// ErrUnrecognizedStatusV2 is an instance of UnrecognizedStatusV2 error status. It's expected to be used for [errors.Is]
// and MUST NOT be changed.
var ErrUnrecognizedStatusV2 UnrecognizedStatusV2
// UnrecognizedStatusV2 describes status of the uncertain failure.
// Instances provide [StatusV2] and error interfaces.
type UnrecognizedStatusV2 struct {
v2 status.Status
}
func (x unrecognizedStatusV2) Error() string {
func (x UnrecognizedStatusV2) Error() string {
return errMessageStatusV2("unrecognized", x.v2.Message())
}
// implements local interface defined in FromStatusV2 func.
func (x *unrecognizedStatusV2) fromStatusV2(st *status.Status) {
// Is implements interface for correct checking current error type with [errors.Is].
func (x UnrecognizedStatusV2) Is(target error) bool {
switch target.(type) {
default:
return false
case UnrecognizedStatusV2, *UnrecognizedStatusV2:
return true
}
}
// implements local interface defined in [ErrorFromV2] func.
func (x *UnrecognizedStatusV2) fromStatusV2(st *status.Status) {
x.v2 = *st
}

View file

@ -1,6 +1,7 @@
package apistatus
import (
"errors"
"fmt"
"github.com/nspcc-dev/neofs-api-go/v2/container"
@ -9,38 +10,50 @@ import (
"github.com/nspcc-dev/neofs-api-go/v2/status"
)
// StatusV2 defines a variety of Status instances compatible with NeoFS API V2 protocol.
// StatusV2 defines a variety of status instances compatible with NeoFS API V2 protocol.
//
// Note: it is not recommended to use this type directly, it is intended for documentation of the library functionality.
type StatusV2 interface {
Status
// ToStatusV2 returns the status as github.com/nspcc-dev/neofs-api-go/v2/status.Status message structure.
ToStatusV2() *status.Status
// ErrorToV2 returns the status as github.com/nspcc-dev/neofs-api-go/v2/status.Status message structure.
ErrorToV2() *status.Status
}
// FromStatusV2 converts status.Status message structure to Status instance. Inverse to ToStatusV2 operation.
// ErrorFromV2 converts [status.Status] message structure to error. Inverse to [ErrorToV2] operation.
//
// If result is not nil, it implements StatusV2. This fact should be taken into account only when passing
// the result to the inverse function ToStatusV2, casts are not compatibility-safe.
// If result is not nil, it implements [StatusV2]. This fact should be taken into account only when passing
// the result to the inverse function [ErrorToV2], casts are not compatibility-safe.
//
// Below is the mapping of return codes to Status instance types (with a description of parsing details).
// Below is the mapping of return codes to status instance types (with a description of parsing details).
// Note: notice if the return type is a pointer.
//
// Successes:
// - status.OK: *SuccessDefaultV2 (this also includes nil argument).
// - [status.OK]: nil (this also includes nil argument).
//
// Common failures:
// - status.Internal: *ServerInternal;
// - status.SignatureVerificationFail: *SignatureVerification.
// - [status.Internal]: *[ServerInternal];
// - [status.SignatureVerificationFail]: *[SignatureVerification].
// - [status.WrongMagicNumber]: *[WrongMagicNumber].
// - [status.NodeUnderMaintenance]: *[NodeUnderMaintenance].
//
// Object failures:
// - object.StatusLocked: *ObjectLocked;
// - object.StatusLockNonRegularObject: *LockNonRegularObject.
// - object.StatusAccessDenied: *ObjectAccessDenied.
func FromStatusV2(st *status.Status) Status {
// - [object.StatusLocked]: *[ObjectLocked];
// - [object.StatusLockNonRegularObject]: *[LockNonRegularObject].
// - [object.StatusAccessDenied]: *[ObjectAccessDenied].
// - [object.StatusNotFound]: *[ObjectNotFound].
// - [object.StatusAlreadyRemoved]: *[ObjectAlreadyRemoved].
// - [object.StatusOutOfRange]: *[ObjectOutOfRange].
//
// Container failures:
// - [container.StatusNotFound]: *[ContainerNotFound];
// - [container.StatusEACLNotFound]: *[EACLNotFound];
//
// Session failures:
// - [session.StatusTokenNotFound]: *[SessionTokenNotFound];
// - [session.StatusTokenExpired]: *[SessionTokenExpired];
func ErrorFromV2(st *status.Status) error {
var decoder interface {
fromStatusV2(*status.Status)
Error() string
}
switch code := st.Code(); {
@ -48,7 +61,7 @@ func FromStatusV2(st *status.Status) Status {
//nolint:exhaustive
switch status.LocalizeSuccess(&code); code {
case status.OK:
decoder = new(SuccessDefaultV2)
return nil
}
case status.IsCommonFail(code):
switch status.LocalizeCommonFail(&code); code {
@ -95,7 +108,7 @@ func FromStatusV2(st *status.Status) Status {
}
if decoder == nil {
decoder = new(unrecognizedStatusV2)
decoder = new(UnrecognizedStatusV2)
}
decoder.fromStatusV2(st)
@ -103,27 +116,28 @@ func FromStatusV2(st *status.Status) Status {
return decoder
}
// ToStatusV2 converts Status instance to status.Status message structure. Inverse to FromStatusV2 operation.
// ErrorToV2 converts error to status.Status message structure. Inverse to [ErrorFromV2] operation.
//
// If argument is the StatusV2 instance, it is converted directly.
// Otherwise, successes are converted with status.OK code w/o details and message,
// failures - with status.Internal and error text message w/o details.
func ToStatusV2(st Status) *status.Status {
if v, ok := st.(StatusV2); ok {
return v.ToStatusV2()
}
if IsSuccessful(st) {
// If argument is the [StatusV2] instance, it is converted directly.
// Otherwise, successes are converted with [status.OK] code w/o details and message,
// failures - with [status.Internal] and error text message w/o details.
func ErrorToV2(err error) *status.Status {
if err == nil {
return newStatusV2WithLocalCode(status.OK, status.GlobalizeSuccess)
}
var instance StatusV2
if errors.As(err, &instance) {
return instance.ErrorToV2()
}
internalErrorStatus := newStatusV2WithLocalCode(status.Internal, status.GlobalizeCommonFail)
internalErrorStatus.SetMessage(st.(error).Error()) // type cast never panics because IsSuccessful() checks cast
internalErrorStatus.SetMessage(err.Error())
return internalErrorStatus
}
func errMessageStatusV2(code interface{}, msg string) string {
func errMessageStatusV2(code any, msg string) string {
const (
noMsgFmt = "status: code = %v"
msgFmt = noMsgFmt + " message = %s"

View file

@ -8,292 +8,189 @@ import (
"github.com/stretchr/testify/require"
)
func TestToStatusV2(t *testing.T) {
type statusConstructor func() apistatus.Status
for _, testItem := range [...]struct {
status interface{} // Status or statusConstructor
codeV2 uint64
messageV2 string
}{
{
status: errors.New("some error"),
codeV2: 1024,
messageV2: "some error",
},
{
status: 1,
codeV2: 0,
},
{
status: "text",
codeV2: 0,
},
{
status: true,
codeV2: 0,
},
{
status: true,
codeV2: 0,
},
{
status: nil,
codeV2: 0,
},
{
status: (statusConstructor)(func() apistatus.Status {
var st apistatus.ServerInternal
st.SetMessage("internal error message")
return st
}),
codeV2: 1024,
},
{
status: (statusConstructor)(func() apistatus.Status {
var st apistatus.WrongMagicNumber
st.WriteCorrectMagic(322)
return st
}),
codeV2: 1025,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.ObjectLocked)
}),
codeV2: 2050,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.LockNonRegularObject)
}),
codeV2: 2051,
},
{
status: (statusConstructor)(func() apistatus.Status {
var st apistatus.ObjectAccessDenied
st.WriteReason("any reason")
return st
}),
codeV2: 2048,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.ObjectNotFound)
}),
codeV2: 2049,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.ObjectAlreadyRemoved)
}),
codeV2: 2052,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.ObjectOutOfRange)
}),
codeV2: 2053,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.ContainerNotFound)
}),
codeV2: 3072,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.EACLNotFound)
}),
codeV2: 3073,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.SessionTokenNotFound)
}),
codeV2: 4096,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.SessionTokenExpired)
}),
codeV2: 4097,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.NodeUnderMaintenance)
}),
codeV2: 1027,
},
} {
var st apistatus.Status
if cons, ok := testItem.status.(statusConstructor); ok {
st = cons()
} else {
st = testItem.status
}
stv2 := apistatus.ToStatusV2(st)
// must generate the same status.Status message
require.EqualValues(t, testItem.codeV2, stv2.Code())
if len(testItem.messageV2) > 0 {
require.Equal(t, testItem.messageV2, stv2.Message())
}
_, ok := st.(apistatus.StatusV2)
if ok {
// restore and convert again
restored := apistatus.FromStatusV2(stv2)
res := apistatus.ToStatusV2(restored)
// must generate the same status.Status message
require.Equal(t, stv2, res)
}
}
}
func TestFromStatusV2(t *testing.T) {
type statusConstructor func() apistatus.Status
type statusConstructor func() error
for _, testItem := range [...]struct {
status interface{} // Status or statusConstructor
status any // Status or statusConstructor
codeV2 uint64
messageV2 string
compatibleErrs []error
checkAsErr func(error) bool
}{
{
status: errors.New("some error"),
status: (statusConstructor)(func() error {
return errors.New("some error")
}),
codeV2: 1024,
messageV2: "some error",
},
{
status: 1,
status: (statusConstructor)(func() error {
return nil
}),
codeV2: 0,
},
{
status: "text",
codeV2: 0,
},
{
status: true,
codeV2: 0,
},
{
status: true,
codeV2: 0,
},
{
status: nil,
codeV2: 0,
},
{
status: (statusConstructor)(func() apistatus.Status {
var st apistatus.ServerInternal
status: (statusConstructor)(func() error {
st := new(apistatus.ServerInternal)
st.SetMessage("internal error message")
return st
}),
codeV2: 1024,
compatibleErrs: []error{apistatus.ErrServerInternal, apistatus.ServerInternal{}, &apistatus.ServerInternal{}, apistatus.Error},
checkAsErr: func(err error) bool {
var target *apistatus.ServerInternal
return errors.As(err, &target)
},
},
{
status: (statusConstructor)(func() apistatus.Status {
var st apistatus.WrongMagicNumber
status: (statusConstructor)(func() error {
st := new(apistatus.WrongMagicNumber)
st.WriteCorrectMagic(322)
return st
}),
codeV2: 1025,
compatibleErrs: []error{apistatus.ErrWrongMagicNumber, apistatus.WrongMagicNumber{}, &apistatus.WrongMagicNumber{}, apistatus.Error},
checkAsErr: func(err error) bool {
var target *apistatus.WrongMagicNumber
return errors.As(err, &target)
},
},
{
status: (statusConstructor)(func() apistatus.Status {
status: (statusConstructor)(func() error {
return new(apistatus.ObjectLocked)
}),
codeV2: 2050,
compatibleErrs: []error{apistatus.ErrObjectLocked, apistatus.ObjectLocked{}, &apistatus.ObjectLocked{}, apistatus.Error},
checkAsErr: func(err error) bool {
var target *apistatus.ObjectLocked
return errors.As(err, &target)
},
},
{
status: (statusConstructor)(func() apistatus.Status {
status: (statusConstructor)(func() error {
return new(apistatus.LockNonRegularObject)
}),
codeV2: 2051,
compatibleErrs: []error{apistatus.ErrLockNonRegularObject, apistatus.LockNonRegularObject{}, &apistatus.LockNonRegularObject{}, apistatus.Error},
checkAsErr: func(err error) bool {
var target *apistatus.LockNonRegularObject
return errors.As(err, &target)
},
},
{
status: (statusConstructor)(func() apistatus.Status {
var st apistatus.ObjectAccessDenied
status: (statusConstructor)(func() error {
st := new(apistatus.ObjectAccessDenied)
st.WriteReason("any reason")
return st
}),
codeV2: 2048,
compatibleErrs: []error{apistatus.ErrObjectAccessDenied, apistatus.ObjectAccessDenied{}, &apistatus.ObjectAccessDenied{}, apistatus.Error},
checkAsErr: func(err error) bool {
var target *apistatus.ObjectAccessDenied
return errors.As(err, &target)
},
},
{
status: (statusConstructor)(func() apistatus.Status {
status: (statusConstructor)(func() error {
return new(apistatus.ObjectNotFound)
}),
codeV2: 2049,
compatibleErrs: []error{apistatus.ErrObjectNotFound, apistatus.ObjectNotFound{}, &apistatus.ObjectNotFound{}, apistatus.Error},
checkAsErr: func(err error) bool {
var target *apistatus.ObjectNotFound
return errors.As(err, &target)
},
},
{
status: (statusConstructor)(func() apistatus.Status {
status: (statusConstructor)(func() error {
return new(apistatus.ObjectAlreadyRemoved)
}),
codeV2: 2052,
compatibleErrs: []error{apistatus.ErrObjectAlreadyRemoved, apistatus.ObjectAlreadyRemoved{}, &apistatus.ObjectAlreadyRemoved{}, apistatus.Error},
checkAsErr: func(err error) bool {
var target *apistatus.ObjectAlreadyRemoved
return errors.As(err, &target)
},
},
{
status: statusConstructor(func() apistatus.Status {
status: statusConstructor(func() error {
return new(apistatus.ObjectOutOfRange)
}),
codeV2: 2053,
compatibleErrs: []error{apistatus.ErrObjectOutOfRange, apistatus.ObjectOutOfRange{}, &apistatus.ObjectOutOfRange{}, apistatus.Error},
checkAsErr: func(err error) bool {
var target *apistatus.ObjectOutOfRange
return errors.As(err, &target)
},
},
{
status: (statusConstructor)(func() apistatus.Status {
status: (statusConstructor)(func() error {
return new(apistatus.ContainerNotFound)
}),
codeV2: 3072,
compatibleErrs: []error{apistatus.ErrContainerNotFound, apistatus.ContainerNotFound{}, &apistatus.ContainerNotFound{}, apistatus.Error},
checkAsErr: func(err error) bool {
var target *apistatus.ContainerNotFound
return errors.As(err, &target)
},
},
{
status: (statusConstructor)(func() apistatus.Status {
status: (statusConstructor)(func() error {
return new(apistatus.EACLNotFound)
}),
codeV2: 3073,
compatibleErrs: []error{apistatus.ErrEACLNotFound, apistatus.EACLNotFound{}, &apistatus.EACLNotFound{}, apistatus.Error},
checkAsErr: func(err error) bool {
var target *apistatus.EACLNotFound
return errors.As(err, &target)
},
},
{
status: (statusConstructor)(func() apistatus.Status {
status: (statusConstructor)(func() error {
return new(apistatus.SessionTokenNotFound)
}),
codeV2: 4096,
compatibleErrs: []error{apistatus.ErrSessionTokenNotFound, apistatus.SessionTokenNotFound{}, &apistatus.SessionTokenNotFound{}, apistatus.Error},
checkAsErr: func(err error) bool {
var target *apistatus.SessionTokenNotFound
return errors.As(err, &target)
},
},
{
status: (statusConstructor)(func() apistatus.Status {
status: (statusConstructor)(func() error {
return new(apistatus.SessionTokenExpired)
}),
codeV2: 4097,
compatibleErrs: []error{apistatus.ErrSessionTokenExpired, apistatus.SessionTokenExpired{}, &apistatus.SessionTokenExpired{}, apistatus.Error},
checkAsErr: func(err error) bool {
var target *apistatus.SessionTokenExpired
return errors.As(err, &target)
},
},
{
status: (statusConstructor)(func() apistatus.Status {
status: (statusConstructor)(func() error {
return new(apistatus.NodeUnderMaintenance)
}),
codeV2: 1027,
compatibleErrs: []error{apistatus.ErrNodeUnderMaintenance, apistatus.NodeUnderMaintenance{}, &apistatus.NodeUnderMaintenance{}, apistatus.Error},
checkAsErr: func(err error) bool {
var target *apistatus.NodeUnderMaintenance
return errors.As(err, &target)
},
},
} {
var st apistatus.Status
var st error
cons, ok := testItem.status.(statusConstructor)
require.True(t, ok)
if cons, ok := testItem.status.(statusConstructor); ok {
st = cons()
} else {
st = testItem.status
}
stv2 := apistatus.ToStatusV2(st)
stv2 := apistatus.ErrorToV2(st)
// must generate the same status.Status message
require.EqualValues(t, testItem.codeV2, stv2.Code())
@ -301,15 +198,25 @@ func TestFromStatusV2(t *testing.T) {
require.Equal(t, testItem.messageV2, stv2.Message())
}
_, ok := st.(apistatus.StatusV2)
_, ok = st.(apistatus.StatusV2)
if ok {
// restore and convert again
restored := apistatus.FromStatusV2(stv2)
restored := apistatus.ErrorFromV2(stv2)
res := apistatus.ToStatusV2(restored)
res := apistatus.ErrorToV2(restored)
// must generate the same status.Status message
require.Equal(t, stv2, res)
}
randomError := errors.New("garbage")
for _, err := range testItem.compatibleErrs {
require.ErrorIs(t, st, err)
require.NotErrorIs(t, randomError, err)
}
if testItem.checkAsErr != nil {
require.True(t, testItem.checkAsErr(st))
}
}
}

View file

@ -1,7 +1,6 @@
package container
import (
"crypto/ecdsa"
"crypto/sha256"
"errors"
"fmt"
@ -15,9 +14,7 @@ import (
"github.com/nspcc-dev/neofs-sdk-go/container/acl"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
"github.com/nspcc-dev/neofs-sdk-go/user"
"github.com/nspcc-dev/neofs-sdk-go/version"
)
@ -118,8 +115,6 @@ func (x *Container) readFromV2(m container.Container, checkFieldPresence bool) e
}
switch key {
case container.SysAttributeSubnet:
err = new(subnetid.ID).DecodeString(val)
case attributeTimestamp:
_, err = strconv.ParseInt(val, 10, 64)
}
@ -383,28 +378,6 @@ func CreatedAt(cnr Container) time.Time {
return time.Unix(sec, 0)
}
// SetSubnet places the Container on the specified NeoFS subnet. If called,
// container nodes will only be selected from the given subnet, otherwise from
// the entire network.
func SetSubnet(cnr *Container, subNet subnetid.ID) {
cnr.SetAttribute(container.SysAttributeSubnet, subNet.EncodeToString())
}
// Subnet return container subnet set using SetSubnet.
//
// Zero Container is bound to zero subnet.
func Subnet(cnr Container) (res subnetid.ID) {
val := cnr.Attribute(container.SysAttributeSubnet)
if val != "" {
err := res.DecodeString(val)
if err != nil {
panic(fmt.Sprintf("invalid subnet attribute: %s (%v)", val, err))
}
}
return
}
const attributeHomoHashEnabled = "true"
// DisableHomomorphicHashing sets flag to disable homomorphic hashing of the
@ -478,11 +451,19 @@ func ReadDomain(cnr Container) (res Domain) {
// and writes it into dst. Signature instance MUST NOT be nil. CalculateSignature
// is expected to be called after all the Container data is filled and before
// saving the Container in the NeoFS network. Note that мany subsequent change
// will most likely break the signature.
// will most likely break the signature. signer MUST be of
// [neofscrypto.ECDSA_DETERMINISTIC_SHA256] scheme, for example, [neofsecdsa.SignerRFC6979]
// can be used.
//
// See also VerifySignature.
func CalculateSignature(dst *neofscrypto.Signature, cnr Container, signer ecdsa.PrivateKey) error {
return dst.Calculate(neofsecdsa.SignerRFC6979(signer), cnr.Marshal())
//
// Returned errors:
// - [neofscrypto.ErrIncorrectSigner]
func CalculateSignature(dst *neofscrypto.Signature, cnr Container, signer neofscrypto.Signer) error {
if signer.Scheme() != neofscrypto.ECDSA_DETERMINISTIC_SHA256 {
return fmt.Errorf("%w: expected ECDSA_DETERMINISTIC_SHA256 scheme", neofscrypto.ErrIncorrectSigner)
}
return dst.Calculate(signer, cnr.Marshal())
}
// VerifySignature verifies Container signature calculated using CalculateSignature.

View file

@ -7,7 +7,6 @@ import (
"time"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
v2container "github.com/nspcc-dev/neofs-api-go/v2/container"
v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
@ -16,16 +15,15 @@ import (
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
containertest "github.com/nspcc-dev/neofs-sdk-go/container/test"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
"github.com/nspcc-dev/neofs-sdk-go/crypto/test"
netmaptest "github.com/nspcc-dev/neofs-sdk-go/netmap/test"
subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
subnetidtest "github.com/nspcc-dev/neofs-sdk-go/subnet/id/test"
usertest "github.com/nspcc-dev/neofs-sdk-go/user/test"
"github.com/nspcc-dev/neofs-sdk-go/version"
"github.com/stretchr/testify/require"
)
func TestPlacementPolicyEncoding(t *testing.T) {
v := containertest.Container()
v := containertest.Container(t)
t.Run("binary", func(t *testing.T) {
var v2 container.Container
@ -46,7 +44,7 @@ func TestPlacementPolicyEncoding(t *testing.T) {
}
func TestContainer_Init(t *testing.T) {
val := containertest.Container()
val := containertest.Container(t)
val.Init()
@ -78,9 +76,9 @@ func TestContainer_Owner(t *testing.T) {
require.Zero(t, val.Owner())
val = containertest.Container()
val = containertest.Container(t)
owner := *usertest.ID()
owner := *usertest.ID(t)
val.SetOwner(owner)
@ -103,7 +101,7 @@ func TestContainer_BasicACL(t *testing.T) {
require.Zero(t, val.BasicACL())
val = containertest.Container()
val = containertest.Container(t)
basicACL := containertest.BasicACL()
val.SetBasicACL(basicACL)
@ -124,7 +122,7 @@ func TestContainer_PlacementPolicy(t *testing.T) {
require.Zero(t, val.PlacementPolicy())
val = containertest.Container()
val = containertest.Container(t)
pp := netmaptest.PlacementPolicy()
val.SetPlacementPolicy(pp)
@ -155,7 +153,7 @@ func TestContainer_Attribute(t *testing.T) {
const attrKey1, attrKey2 = "key1", "key2"
const attrVal1, attrVal2 = "val1", "val2"
val := containertest.Container()
val := containertest.Container(t)
val.SetAttribute(attrKey1, attrVal1)
val.SetAttribute(attrKey2, attrVal2)
@ -194,7 +192,7 @@ func TestSetName(t *testing.T) {
container.SetName(&val, "")
})
val = containertest.Container()
val = containertest.Container(t)
const name = "some name"
@ -216,7 +214,7 @@ func TestSetCreationTime(t *testing.T) {
require.Zero(t, container.CreatedAt(val).Unix())
val = containertest.Container()
val = containertest.Container(t)
creat := time.Now()
@ -233,34 +231,12 @@ func TestSetCreationTime(t *testing.T) {
require.Equal(t, creat.Unix(), container.CreatedAt(val2).Unix())
}
func TestSetSubnet(t *testing.T) {
var val container.Container
require.True(t, subnetid.IsZero(container.Subnet(val)))
val = containertest.Container()
sub := subnetidtest.ID()
container.SetSubnet(&val, sub)
var msg v2container.Container
val.WriteToV2(&msg)
assertContainsAttribute(t, msg, v2container.SysAttributeSubnet, sub.EncodeToString())
var val2 container.Container
require.NoError(t, val2.ReadFromV2(msg))
require.Equal(t, sub, container.Subnet(val))
}
func TestDisableHomomorphicHashing(t *testing.T) {
var val container.Container
require.False(t, container.IsHomomorphicHashingDisabled(val))
val = containertest.Container()
val = containertest.Container(t)
container.DisableHomomorphicHashing(&val)
@ -280,7 +256,7 @@ func TestWriteDomain(t *testing.T) {
require.Zero(t, container.ReadDomain(val).Name())
val = containertest.Container()
val = containertest.Container(t)
const name = "domain name"
@ -312,7 +288,7 @@ func TestWriteDomain(t *testing.T) {
}
func TestCalculateID(t *testing.T) {
val := containertest.Container()
val := containertest.Container(t)
require.False(t, container.AssertID(cidtest.ID(), val))
@ -332,14 +308,12 @@ func TestCalculateID(t *testing.T) {
}
func TestCalculateSignature(t *testing.T) {
key, err := keys.NewPrivateKey()
require.NoError(t, err)
val := containertest.Container()
val := containertest.Container(t)
var sig neofscrypto.Signature
require.NoError(t, container.CalculateSignature(&sig, val, key.PrivateKey))
require.Error(t, container.CalculateSignature(&sig, val, test.RandomSigner(t)))
require.NoError(t, container.CalculateSignature(&sig, val, test.RandomSignerRFC6979(t)))
var msg refs.Signature
sig.WriteToV2(&msg)

View file

@ -10,7 +10,7 @@ import (
)
func TestContainer_NetworkConfig(t *testing.T) {
c := containertest.Container()
c := containertest.Container(t)
nc := netmaptest.NetworkInfo()
t.Run("default", func(t *testing.T) {

View file

@ -2,6 +2,7 @@ package containertest
import (
"math/rand"
"testing"
"github.com/nspcc-dev/neofs-sdk-go/container"
"github.com/nspcc-dev/neofs-sdk-go/container/acl"
@ -11,8 +12,8 @@ import (
)
// Container returns random container.Container.
func Container() (x container.Container) {
owner := usertest.ID()
func Container(t *testing.T) (x container.Container) {
owner := usertest.ID(t)
x.Init()
x.SetAttribute("some attribute", "value")

View file

@ -1,11 +1,18 @@
package neofscrypto
import (
"errors"
"fmt"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
)
// ErrIncorrectSigner is returned from function when the signer passed to it
// is incompatible with the function requirements. This variable is intended
// to be used as documentation and for [errors.Is] purposes and MUST NOT be
// changed.
var ErrIncorrectSigner = errors.New("incorrect signer")
// Scheme represents digital signature algorithm with fixed cryptographic hash function.
//
// Negative values are reserved and depend on context (e.g. unsupported scheme).
@ -92,3 +99,38 @@ type PublicKey interface {
// Verify checks signature of the given data. True means correct signature.
Verify(data, signature []byte) bool
}
// StaticSigner emulates real sign and contains already precalculated hash.
// Provides neofscrypto.Signer interface.
type StaticSigner struct {
scheme Scheme
sig []byte
pubKey PublicKey
}
// NewStaticSigner creates new StaticSigner.
func NewStaticSigner(scheme Scheme, sig []byte, pubKey PublicKey) *StaticSigner {
return &StaticSigner{
scheme: scheme,
sig: sig,
pubKey: pubKey,
}
}
// Scheme returns neofscrypto.ECDSA_DETERMINISTIC_SHA256.
// Implements neofscrypto.Signer.
func (s *StaticSigner) Scheme() Scheme {
return s.scheme
}
// Sign returns precalculated hash.
// Implements neofscrypto.Signer.
func (s *StaticSigner) Sign(_ []byte) ([]byte, error) {
return s.sig, nil
}
// Public returns neofscrypto.PublicKey.
// Implements neofscrypto.Signer.
func (s *StaticSigner) Public() PublicKey {
return s.pubKey
}

33
crypto/test/tests.go Normal file
View file

@ -0,0 +1,33 @@
/*
Package tests provides special help functions for testing NeoFS API and its environment.
All functions accepting `t *testing.T` that emphasize there are only for tests purposes.
*/
package test
import (
"testing"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa"
"github.com/stretchr/testify/require"
)
// RandomSigner return neofscrypto.Signer ONLY for TESTs purposes.
// It may be used like helper to get new neofscrypto.Signer if you need it in yours tests.
func RandomSigner(tb testing.TB) neofscrypto.Signer {
p, err := keys.NewPrivateKey()
require.NoError(tb, err)
return neofsecdsa.Signer(p.PrivateKey)
}
// RandomSignerRFC6979 return neofscrypto.Signer ONLY for TESTs purposes.
// It may be used like helper to get new neofscrypto.Signer if you need it in yours tests.
func RandomSignerRFC6979(tb testing.TB) neofscrypto.Signer {
p, err := keys.NewPrivateKey()
require.NoError(tb, err)
return neofsecdsa.SignerRFC6979(p.PrivateKey)
}

View file

@ -127,21 +127,30 @@ func ActionFromV2(action v2acl.Action) (a Action) {
return a
}
// String returns string representation of Action.
// EncodeToString returns string representation of Action.
//
// String mapping:
// - ActionAllow: ALLOW;
// - ActionDeny: DENY;
// - ActionUnknown, default: ACTION_UNSPECIFIED.
func (a Action) String() string {
func (a Action) EncodeToString() string {
return a.ToV2().String()
}
// FromString parses Action from a string representation.
// It is a reverse action to String().
// String implements fmt.Stringer.
//
// String is designed to be human-readable, and its format MAY differ between
// SDK versions. String MAY return same result as EncodeToString. String MUST NOT
// be used to encode ID into NeoFS protocol string.
func (a Action) String() string {
return a.EncodeToString()
}
// DecodeString parses Action from a string representation.
// It is a reverse action to EncodeToString().
//
// Returns true if s was parsed successfully.
func (a *Action) FromString(s string) bool {
func (a *Action) DecodeString(s string) bool {
var g v2acl.Action
ok := g.FromString(s)
@ -199,7 +208,7 @@ func OperationFromV2(operation v2acl.Operation) (o Operation) {
return o
}
// String returns string representation of Operation.
// EncodeToString returns string representation of Operation.
//
// String mapping:
// - OperationGet: GET;
@ -210,15 +219,24 @@ func OperationFromV2(operation v2acl.Operation) (o Operation) {
// - OperationRange: GETRANGE;
// - OperationRangeHash: GETRANGEHASH;
// - OperationUnknown, default: OPERATION_UNSPECIFIED.
func (o Operation) String() string {
func (o Operation) EncodeToString() string {
return o.ToV2().String()
}
// FromString parses Operation from a string representation.
// It is a reverse action to String().
// String implements fmt.Stringer.
//
// String is designed to be human-readable, and its format MAY differ between
// SDK versions. String MAY return same result as EncodeToString. String MUST NOT
// be used to encode ID into NeoFS protocol string.
func (o Operation) String() string {
return o.EncodeToString()
}
// DecodeString parses Operation from a string representation.
// It is a reverse action to EncodeToString().
//
// Returns true if s was parsed successfully.
func (o *Operation) FromString(s string) bool {
func (o *Operation) DecodeString(s string) bool {
var g v2acl.Operation
ok := g.FromString(s)
@ -260,22 +278,31 @@ func RoleFromV2(role v2acl.Role) (r Role) {
return r
}
// String returns string representation of Role.
// EncodeToString returns string representation of Role.
//
// String mapping:
// - RoleUser: USER;
// - RoleSystem: SYSTEM;
// - RoleOthers: OTHERS;
// - RoleUnknown, default: ROLE_UNKNOWN.
func (r Role) String() string {
func (r Role) EncodeToString() string {
return r.ToV2().String()
}
// FromString parses Role from a string representation.
// It is a reverse action to String().
// String implements fmt.Stringer.
//
// String is designed to be human-readable, and its format MAY differ between
// SDK versions. String MAY return same result as EncodeToString. String MUST NOT
// be used to encode ID into NeoFS protocol string.
func (r Role) String() string {
return r.EncodeToString()
}
// DecodeString parses Role from a string representation.
// It is a reverse action to EncodeToString().
//
// Returns true if s was parsed successfully.
func (r *Role) FromString(s string) bool {
func (r *Role) DecodeString(s string) bool {
var g v2acl.Role
ok := g.FromString(s)
@ -313,21 +340,30 @@ func MatchFromV2(match v2acl.MatchType) (m Match) {
return m
}
// String returns string representation of Match.
// EncodeToString returns string representation of Match.
//
// String mapping:
// - MatchStringEqual: STRING_EQUAL;
// - MatchStringNotEqual: STRING_NOT_EQUAL;
// - MatchUnknown, default: MATCH_TYPE_UNSPECIFIED.
func (m Match) String() string {
func (m Match) EncodeToString() string {
return m.ToV2().String()
}
// FromString parses Match from a string representation.
// It is a reverse action to String().
// String implements fmt.Stringer.
//
// String is designed to be human-readable, and its format MAY differ between
// SDK versions. String MAY return same result as EncodeToString. String MUST NOT
// be used to encode ID into NeoFS protocol string.
func (m Match) String() string {
return m.EncodeToString()
}
// DecodeString parses Match from a string representation.
// It is a reverse action to EncodeToString().
//
// Returns true if s was parsed successfully.
func (m *Match) FromString(s string) bool {
func (m *Match) DecodeString(s string) bool {
var g v2acl.MatchType
ok := g.FromString(s)
@ -369,21 +405,30 @@ func FilterHeaderTypeFromV2(header v2acl.HeaderType) (h FilterHeaderType) {
return h
}
// String returns string representation of FilterHeaderType.
// EncodeToString returns string representation of FilterHeaderType.
//
// String mapping:
// - HeaderFromRequest: REQUEST;
// - HeaderFromObject: OBJECT;
// - HeaderTypeUnknown, default: HEADER_UNSPECIFIED.
func (h FilterHeaderType) String() string {
func (h FilterHeaderType) EncodeToString() string {
return h.ToV2().String()
}
// FromString parses FilterHeaderType from a string representation.
// It is a reverse action to String().
// String implements fmt.Stringer.
//
// String is designed to be human-readable, and its format MAY differ between
// SDK versions. String MAY return same result as EncodeToString. String MUST NOT
// be used to encode ID into NeoFS protocol string.
func (h FilterHeaderType) String() string {
return h.EncodeToString()
}
// DecodeString parses FilterHeaderType from a string representation.
// It is a reverse action to EncodeToString().
//
// Returns true if s was parsed successfully.
func (h *FilterHeaderType) FromString(s string) bool {
func (h *FilterHeaderType) DecodeString(s string) bool {
var g v2acl.HeaderType
ok := g.FromString(s)

View file

@ -118,8 +118,8 @@ func TestFilterHeaderType(t *testing.T) {
}
type enumIface interface {
FromString(string) bool
String() string
DecodeString(string) bool
EncodeToString() string
}
type enumStringItem struct {
@ -129,11 +129,11 @@ type enumStringItem struct {
func testEnumStrings(t *testing.T, e enumIface, items []enumStringItem) {
for _, item := range items {
require.Equal(t, item.str, item.val.String())
require.Equal(t, item.str, item.val.EncodeToString())
s := item.val.String()
s := item.val.EncodeToString()
require.True(t, e.FromString(s), s)
require.True(t, e.DecodeString(s), s)
require.EqualValues(t, item.val, e, item.val)
}
@ -143,7 +143,7 @@ func testEnumStrings(t *testing.T, e enumIface, items []enumStringItem) {
"some string",
"UNSPECIFIED",
} {
require.False(t, e.FromString(str))
require.False(t, e.DecodeString(str))
}
}

View file

@ -146,7 +146,7 @@ func (r *Record) AddObjectPayloadHashFilter(m Match, h checksum.Checksum) {
// AddObjectTypeFilter adds filter by object type.
func (r *Record) AddObjectTypeFilter(m Match, t object.Type) {
r.addObjectReservedFilter(m, fKeyObjType, staticStringer(t.String()))
r.addObjectReservedFilter(m, fKeyObjType, staticStringer(t.EncodeToString()))
}
// AddObjectHomomorphicHashFilter adds filter by object payload homomorphic hash value.

View file

@ -154,7 +154,7 @@ func TestReservedRecords(t *testing.T) {
v = versiontest.Version()
oid = oidtest.ID()
cid = cidtest.ID()
ownerid = usertest.ID()
ownerid = usertest.ID(t)
h = checksumtest.Checksum()
typ = new(object.Type)
)
@ -211,7 +211,7 @@ func TestReservedRecords(t *testing.T) {
},
{
f: func(r *Record) {
require.True(t, typ.FromString("REGULAR"))
require.True(t, typ.DecodeString("REGULAR"))
r.AddObjectTypeFilter(MatchStringEqual, *typ)
},
key: v2acl.FilterObjectType,
@ -219,7 +219,7 @@ func TestReservedRecords(t *testing.T) {
},
{
f: func(r *Record) {
require.True(t, typ.FromString("TOMBSTONE"))
require.True(t, typ.DecodeString("TOMBSTONE"))
r.AddObjectTypeFilter(MatchStringEqual, *typ)
},
key: v2acl.FilterObjectType,
@ -227,7 +227,7 @@ func TestReservedRecords(t *testing.T) {
},
{
f: func(r *Record) {
require.True(t, typ.FromString("STORAGE_GROUP"))
require.True(t, typ.DecodeString("STORAGE_GROUP"))
r.AddObjectTypeFilter(MatchStringEqual, *typ)
},
key: v2acl.FilterObjectType,

View file

@ -66,7 +66,7 @@ func TestTable_AddRecord(t *testing.T) {
}
func TestTableEncoding(t *testing.T) {
tab := eacltest.Table()
tab := eacltest.Table(t)
t.Run("binary", func(t *testing.T) {
data, err := tab.Marshal()

View file

@ -1,6 +1,8 @@
package eacltest
import (
"testing"
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
"github.com/nspcc-dev/neofs-sdk-go/eacl"
usertest "github.com/nspcc-dev/neofs-sdk-go/user/test"
@ -21,24 +23,24 @@ func Target() *eacl.Target {
}
// Record returns random eacl.Record.
func Record() *eacl.Record {
func Record(tb testing.TB) *eacl.Record {
x := eacl.NewRecord()
x.SetAction(eacl.ActionAllow)
x.SetOperation(eacl.OperationRangeHash)
x.SetTargets(*Target(), *Target())
x.AddObjectContainerIDFilter(eacl.MatchStringEqual, cidtest.ID())
x.AddObjectOwnerIDFilter(eacl.MatchStringNotEqual, usertest.ID())
x.AddObjectOwnerIDFilter(eacl.MatchStringNotEqual, usertest.ID(tb))
return x
}
func Table() *eacl.Table {
func Table(tb testing.TB) *eacl.Table {
x := eacl.NewTable()
x.SetCID(cidtest.ID())
x.AddRecord(Record())
x.AddRecord(Record())
x.AddRecord(Record(tb))
x.AddRecord(Record(tb))
x.SetVersion(versiontest.Version())
return x

32
go.mod
View file

@ -1,40 +1,40 @@
module github.com/nspcc-dev/neofs-sdk-go
go 1.17
go 1.18
require (
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20221202181307-76fa05c21b12
github.com/google/uuid v1.3.0
github.com/hashicorp/golang-lru v0.5.4
github.com/hashicorp/golang-lru v0.6.0
github.com/mr-tron/base58 v1.2.0
github.com/nspcc-dev/hrw v1.0.9
github.com/nspcc-dev/neo-go v0.99.4
github.com/nspcc-dev/neo-go v0.100.1
github.com/nspcc-dev/neofs-api-go/v2 v2.14.0
github.com/nspcc-dev/neofs-contract v0.16.0
github.com/nspcc-dev/tzhash v1.6.1
github.com/stretchr/testify v1.8.0
github.com/nspcc-dev/tzhash v1.7.0
github.com/stretchr/testify v1.8.1
go.uber.org/atomic v1.10.0
go.uber.org/zap v1.23.0
go.uber.org/zap v1.24.0
)
require (
github.com/btcsuite/btcd v0.22.0-beta // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220927123257-24c107e3a262 // indirect
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20221202075445-cb5c18dc73eb // indirect
github.com/nspcc-dev/neofs-crypto v0.4.0 // indirect
github.com/nspcc-dev/rfc6979 v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.7 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/exp v0.0.0-20221227203929-1b447090c38c // indirect
golang.org/x/net v0.3.0 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.5.0 // indirect
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect
google.golang.org/grpc v1.48.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect

63
go.sum
View file

@ -51,15 +51,14 @@ github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGn
github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521073959-f0d4d129b7f1/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves=
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20221202181307-76fa05c21b12 h1:npHgfD4Tl2WJS3AJaMUi5ynGDPUBfkg3U3fCzDyXZ+4=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20221202181307-76fa05c21b12/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta h1:LTDpDKUM5EeOFBPM8IXpinEcmZ6FWfNZbE3lfrfdnWo=
github.com/btcsuite/btcd v0.22.0-beta/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
@ -86,7 +85,6 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@ -94,6 +92,9 @@ github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -194,8 +195,9 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@ -249,7 +251,6 @@ github.com/nspcc-dev/dbft v0.0.0-20200117124306-478e5cfbf03a/go.mod h1:/YFK+XOxx
github.com/nspcc-dev/dbft v0.0.0-20200219114139-199d286ed6c1/go.mod h1:O0qtn62prQSqizzoagHmuuKoz8QMkU3SzBoKdEvm3aQ=
github.com/nspcc-dev/dbft v0.0.0-20210721160347-1b03241391ac/go.mod h1:U8MSnEShH+o5hexfWJdze6uMFJteP0ko7J2frO7Yu1Y=
github.com/nspcc-dev/dbft v0.0.0-20220629112714-fd49ca59d354/go.mod h1:U8MSnEShH+o5hexfWJdze6uMFJteP0ko7J2frO7Yu1Y=
github.com/nspcc-dev/dbft v0.0.0-20220902113116-58a5e763e647/go.mod h1:g9xisXmX9NP9MjioaTe862n9SlZTrP+6PVUWLBYOr98=
github.com/nspcc-dev/go-ordered-json v0.0.0-20210915112629-e1b6cce73d02/go.mod h1:79bEUDEviBHJMFV6Iq6in57FEOCMcRhfQnfaf0ETA5U=
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 h1:n4ZaFCKt1pQJd7PXoMJabZWK9ejjbLOVrkl/lOUmshg=
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22/go.mod h1:79bEUDEviBHJMFV6Iq6in57FEOCMcRhfQnfaf0ETA5U=
@ -258,11 +259,11 @@ github.com/nspcc-dev/hrw v1.0.9/go.mod h1:l/W2vx83vMQo6aStyx2AuZrJ+07lGv2JQGlVkP
github.com/nspcc-dev/neo-go v0.73.1-pre.0.20200303142215-f5a1b928ce09/go.mod h1:pPYwPZ2ks+uMnlRLUyXOpLieaDQSEaf4NM3zHVbRjmg=
github.com/nspcc-dev/neo-go v0.98.0/go.mod h1:E3cc1x6RXSXrJb2nDWXTXjnXk3rIqVN8YdFyWv+FrqM=
github.com/nspcc-dev/neo-go v0.99.2/go.mod h1:9P0yWqhZX7i/ChJ+zjtiStO1uPTolPFUM+L5oNznU8E=
github.com/nspcc-dev/neo-go v0.99.4 h1:8Y+SdRxksC72a4PNkcGCh/aaQinh9Gu+c5LilbcsXOI=
github.com/nspcc-dev/neo-go v0.99.4/go.mod h1:mKTolfRUfKjFso5HPvGSQtUZc70n0VKBMs16eGuC5gA=
github.com/nspcc-dev/neo-go v0.100.1 h1:yugxbQRdzM+ObVa5mtr9/n4rYjxSIrryne8MVr9NBwU=
github.com/nspcc-dev/neo-go v0.100.1/go.mod h1:Nnp7F4e9IBccsgtCeLtUWV+0T6gk1PtP5HRtA13hUfc=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220809123759-3094d3e0c14b/go.mod h1:23bBw0v6pBYcrWs8CBEEDIEDJNbcFoIh8pGGcf2Vv8s=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220927123257-24c107e3a262 h1:UTmSLZw5OpD/JPE1B5Vf98GF0zu2/Hsqq1lGLtStTUE=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220927123257-24c107e3a262/go.mod h1:23bBw0v6pBYcrWs8CBEEDIEDJNbcFoIh8pGGcf2Vv8s=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20221202075445-cb5c18dc73eb h1:GFxfkpXEYAbMIr69JpKOsQWeLOaGrd49HNAor8uDW+A=
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20221202075445-cb5c18dc73eb/go.mod h1:23bBw0v6pBYcrWs8CBEEDIEDJNbcFoIh8pGGcf2Vv8s=
github.com/nspcc-dev/neofs-api-go/v2 v2.11.0-pre.0.20211201134523-3604d96f3fe1/go.mod h1:oS8dycEh8PPf2Jjp6+8dlwWyEv2Dy77h/XhhcdxYEFs=
github.com/nspcc-dev/neofs-api-go/v2 v2.11.1/go.mod h1:oS8dycEh8PPf2Jjp6+8dlwWyEv2Dy77h/XhhcdxYEFs=
github.com/nspcc-dev/neofs-api-go/v2 v2.14.0 h1:jhuN8Ldqz7WApvUJRFY0bjRXE1R3iCkboMX5QVZhHVk=
@ -279,8 +280,8 @@ github.com/nspcc-dev/neofs-sdk-go v0.0.0-20220113123743-7f3162110659/go.mod h1:/
github.com/nspcc-dev/rfc6979 v0.1.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso=
github.com/nspcc-dev/rfc6979 v0.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE=
github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso=
github.com/nspcc-dev/tzhash v1.6.1 h1:8dUrWFpjkmoHF+7GxuGUmarj9LLHWFcuyF3CTrqq9JE=
github.com/nspcc-dev/tzhash v1.6.1/go.mod h1:BoflzCVp+DO/f1mvbcsJQWoFzidIFBhWFZMglbUW648=
github.com/nspcc-dev/tzhash v1.7.0 h1:/+aL33NC7y5OIGnY2kYgjZt8mg7LVGFMdj/KAJLndnk=
github.com/nspcc-dev/tzhash v1.7.0/go.mod h1:Dnx9LUlOLr5paL2Rtc96x0PPs8D9eIkUtowt1n+KQus=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -337,14 +338,16 @@ github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/syndtr/goleveldb v0.0.0-20180307113352-169b1b37be73/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs=
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM=
@ -357,7 +360,6 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/yuin/gopher-lua v0.0.0-20191128022950-c6266f4fe8d7/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
@ -377,15 +379,14 @@ go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -396,8 +397,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -408,6 +410,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20221227203929-1b447090c38c h1:Govq2W3bnHJimHT2ium65kXcI7ZzTniZHcFATnLJM0Q=
golang.org/x/exp v0.0.0-20221227203929-1b447090c38c/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -428,7 +432,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -464,12 +467,12 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -488,8 +491,9 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -538,16 +542,15 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210429154555-c04ba851c2a4/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -557,8 +560,9 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -605,7 +609,6 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -694,7 +697,6 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/abiosoft/ishell.v2 v2.0.0/go.mod h1:sFp+cGtH6o4s1FtpVPTMcHq2yue+c4DGOVohJCPUzwY=
@ -713,7 +715,6 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -6,6 +6,8 @@ import (
"path/filepath"
"testing"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/stretchr/testify/require"
)
@ -37,6 +39,20 @@ func compareNodes(t testing.TB, expected [][]int, nodes nodes, actual [][]NodeIn
}
}
func compareNodesIgnoreOrder(t testing.TB, expected [][]int, nodes nodes, actual [][]NodeInfo) {
require.Equal(t, len(expected), len(actual))
for i := range expected {
require.Equal(t, len(expected[i]), len(actual[i]))
var expectedNodes []NodeInfo
for _, index := range expected[i] {
expectedNodes = append(expectedNodes, nodes[index])
}
require.ElementsMatch(t, expectedNodes, actual[i])
}
}
func TestPlacementPolicy_Interopability(t *testing.T) {
const testsDir = "./json_tests"
@ -62,7 +78,10 @@ func TestPlacementPolicy_Interopability(t *testing.T) {
for name, tt := range tc.Tests {
t.Run(name, func(t *testing.T) {
v, err := nm.ContainerNodes(tt.Policy, tt.Pivot)
var pivot cid.ID
copy(pivot[:], tt.Pivot)
v, err := nm.ContainerNodes(tt.Policy, pivot)
if tt.Result == nil {
require.Error(t, err)
require.Contains(t, err.Error(), tt.Error)
@ -70,10 +89,13 @@ func TestPlacementPolicy_Interopability(t *testing.T) {
require.NoError(t, err)
require.Equal(t, srcNodes, tc.Nodes)
compareNodes(t, tt.Result, tc.Nodes, v)
compareNodesIgnoreOrder(t, tt.Result, tc.Nodes, v)
if tt.Placement.Result != nil {
res, err := nm.PlacementVectors(v, tt.Placement.Pivot)
var placementPivot oid.ID
copy(placementPivot[:], tt.Placement.Pivot)
res, err := nm.PlacementVectors(v, placementPivot)
require.NoError(t, err)
compareNodes(t, tt.Placement.Result, tc.Nodes, res)
require.Equal(t, srcNodes, tc.Nodes)
@ -108,11 +130,14 @@ func BenchmarkPlacementPolicyInteropability(b *testing.B) {
for name, tt := range tc.Tests {
b.Run(name, func(b *testing.B) {
var pivot cid.ID
copy(pivot[:], tt.Pivot)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StartTimer()
v, err := nm.ContainerNodes(tt.Policy, tt.Pivot)
v, err := nm.ContainerNodes(tt.Policy, pivot)
b.StopTimer()
if tt.Result == nil {
require.Error(b, err)
@ -120,11 +145,14 @@ func BenchmarkPlacementPolicyInteropability(b *testing.B) {
} else {
require.NoError(b, err)
compareNodes(b, tt.Result, tc.Nodes, v)
compareNodesIgnoreOrder(b, tt.Result, tc.Nodes, v)
if tt.Placement.Result != nil {
var placementPivot oid.ID
copy(placementPivot[:], tt.Placement.Pivot)
b.StartTimer()
res, err := nm.PlacementVectors(v, tt.Placement.Pivot)
res, err := nm.PlacementVectors(v, placementPivot)
b.StopTimer()
require.NoError(b, err)
compareNodes(b, tt.Placement.Result, tc.Nodes, res)
@ -150,11 +178,14 @@ func BenchmarkManySelects(b *testing.B) {
var nm NetMap
nm.SetNodes(tc.Nodes)
var pivot cid.ID
copy(pivot[:], tt.Pivot)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err = nm.ContainerNodes(tt.Policy, tt.Pivot)
_, err = nm.ContainerNodes(tt.Policy, pivot)
if err != nil {
b.FailNow()
}

View file

@ -183,9 +183,9 @@
"policy": {"replicas":[{"count":1,"selector":"SameRU"},{"count":1,"selector":"DistinctRU"},{"count":1,"selector":"Good"},{"count":1,"selector":"Main"}],"containerBackupFactor":2,"selectors":[{"name":"SameRU","count":2,"clause":"SAME","attribute":"City","filter":"FromRU"},{"name":"DistinctRU","count":2,"clause":"DISTINCT","attribute":"City","filter":"FromRU"},{"name":"Good","count":2,"clause":"DISTINCT","attribute":"Country","filter":"Good"},{"name":"Main","count":3,"clause":"DISTINCT","attribute":"Country","filter":"*"}],"filters":[{"name":"FromRU","key":"Country","op":"EQ","value":"Russia"},{"name":"Good","key":"Rating","op":"GE","value":"4"}]},
"result": [
[0, 5, 9, 10],
[2, 6, 0, 5],
[0, 5, 2, 6],
[1, 8, 2, 5],
[3, 4, 1, 7, 0, 2]
[0, 2, 1, 7, 3, 4]
]
}
}

View file

@ -321,10 +321,10 @@
4
],
[
8,
12,
5,
10
10,
8,
12
]
]
}

View file

@ -1,254 +0,0 @@
{
"name": "subnet tests",
"nodes": [
{
"attributes": [
{
"key": "ID",
"value": "0"
},
{
"key": "City",
"value": "Paris"
},
{
"key": "__NEOFS__SUBNET_0",
"value": "False"
}
],
"state": "UNSPECIFIED"
},
{
"attributes": [
{
"key": "ID",
"value": "1"
},
{
"key": "City",
"value": "Paris"
}
],
"state": "UNSPECIFIED"
},
{
"attributes": [
{
"key": "ID",
"value": "2"
},
{
"key": "City",
"value": "London"
},
{
"key": "__NEOFS__SUBNET_1",
"value": "True"
}
],
"state": "UNSPECIFIED"
},
{
"attributes": [
{
"key": "ID",
"value": "3"
},
{
"key": "City",
"value": "London"
}
],
"state": "UNSPECIFIED"
},
{
"attributes": [
{
"key": "ID",
"value": "4"
},
{
"key": "City",
"value": "Toronto"
},
{
"key": "__NEOFS__SUBNET_1",
"value": "True"
}
],
"state": "UNSPECIFIED"
},
{
"attributes": [
{
"key": "ID",
"value": "5"
},
{
"key": "City",
"value": "Toronto"
},
{
"key": "__NEOFS__SUBNET_2",
"value": "True"
}
],
"state": "UNSPECIFIED"
},
{
"attributes": [
{
"key": "ID",
"value": "6"
},
{
"key": "City",
"value": "Tokyo"
},
{
"key": "__NEOFS__SUBNET_2",
"value": "True"
}
],
"state": "UNSPECIFIED"
},
{
"attributes": [
{
"key": "ID",
"value": "7"
},
{
"key": "City",
"value": "Tokyo"
},
{
"key": "__NEOFS__SUBNET_2",
"value": "True"
}
],
"state": "UNSPECIFIED"
}
],
"tests": {
"select from default subnet, fail": {
"policy": {
"replicas": [
{
"count": 1,
"selector": "S"
}
],
"containerBackupFactor": 0,
"selectors": [
{
"name": "S",
"count": 2,
"clause": "SAME",
"attribute": "City",
"filter": "F"
}
],
"filters": [
{
"name": "F",
"key": "City",
"op": "EQ",
"value": "Paris",
"filters": []
}
],
"subnetId": null
},
"error": "not enough nodes"
},
"select from default subnet, success": {
"policy": {
"replicas": [
{
"count": 1,
"selector": "S"
}
],
"containerBackupFactor": 0,
"selectors": [
{
"name": "S",
"count": 2,
"clause": "SAME",
"attribute": "City",
"filter": "F"
}
],
"filters": [
{
"name": "F",
"key": "City",
"op": "EQ",
"value": "Toronto",
"filters": []
}
],
"subnetId": null
},
"result": [
[
4,
5
]
]
},
"select from non-default subnet, success": {
"policy": {
"replicas": [
{
"count": 3,
"selector": ""
}
],
"containerBackupFactor": 0,
"selectors": [],
"filters": [],
"subnetId": {
"value": 2
}
},
"result": [
[
5,
6,
7
]
]
},
"select subnet via filters": {
"policy": {
"replicas": [
{
"count": 1,
"selector": "S"
}
],
"containerBackupFactor": 1,
"selectors": [
{
"name": "S",
"count": 1,
"clause": "SAME",
"attribute": "City",
"filter": "F"
}
],
"filters": [
{
"name": "F",
"key": "__NEOFS_SUBNET.2.ENABLED",
"op": "EQ",
"value": "True"
}
]
},
"error": "not enough nodes"
}
}
}

View file

@ -1,10 +1,13 @@
package netmap
import (
"crypto/sha256"
"fmt"
"github.com/nspcc-dev/hrw"
"github.com/nspcc-dev/neofs-api-go/v2/netmap"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
)
// NetMap represents NeoFS network map. It includes information about all
@ -140,11 +143,14 @@ func flattenNodes(ns []nodes) nodes {
}
// PlacementVectors sorts container nodes returned by ContainerNodes method
// and returns placement vectors for the entity identified by the given pivot.
// and returns placement vectors for the entity identified by the given object id.
// For example, in order to build node list to store the object, binary-encoded
// object identifier can be used as pivot. Result is deterministic for
// the fixed NetMap and parameters.
func (m NetMap) PlacementVectors(vectors [][]NodeInfo, pivot []byte) ([][]NodeInfo, error) {
func (m NetMap) PlacementVectors(vectors [][]NodeInfo, objectID oid.ID) ([][]NodeInfo, error) {
pivot := make([]byte, sha256.Size)
objectID.Encode(pivot)
h := hrw.Hash(pivot)
wf := defaultWeightFunc(m.nodes)
result := make([][]NodeInfo, len(vectors))
@ -162,15 +168,18 @@ func (m NetMap) PlacementVectors(vectors [][]NodeInfo, pivot []byte) ([][]NodeIn
// given PlacementPolicy to the NetMap. Each line of the list corresponds to a
// replica descriptor. Line order corresponds to order of ReplicaDescriptor list
// in the policy. Nodes are pre-filtered according to the Filter list from
// the policy, and then selected by Selector list. Result is deterministic for
// the fixed NetMap and parameters.
// the policy, and then selected by Selector list. Result is not deterministic and
// node order in each vector may vary for call.
//
// Result can be used in PlacementVectors.
func (m NetMap) ContainerNodes(p PlacementPolicy, pivot []byte) ([][]NodeInfo, error) {
func (m NetMap) ContainerNodes(p PlacementPolicy, containerID cid.ID) ([][]NodeInfo, error) {
c := newContext(m)
c.setPivot(pivot)
c.setCBF(p.backupFactor)
pivot := make([]byte, sha256.Size)
containerID.Encode(pivot)
c.setPivot(pivot)
if err := c.processFilters(p); err != nil {
return nil, err
}

View file

@ -112,6 +112,33 @@ func (x NetworkInfo) WriteToV2(m *netmap.NetworkInfo) {
*m = x.m
}
// Marshal encodes NetworkInfo into a binary format of the NeoFS API protocol
// (Protocol Buffers with direct field order).
//
// See also Unmarshal.
func (x NetworkInfo) Marshal() []byte {
var m netmap.NetworkInfo
x.WriteToV2(&m)
return m.StableMarshal(nil)
}
// Unmarshal decodes NeoFS API protocol binary format into the NetworkInfo
// (Protocol Buffers with direct field order). Returns an error describing
// a format violation.
//
// See also Marshal.
func (x *NetworkInfo) Unmarshal(data []byte) error {
var m netmap.NetworkInfo
err := m.Unmarshal(data)
if err != nil {
return err
}
return x.readFromV2(m, false)
}
// CurrentEpoch returns epoch set using SetCurrentEpoch.
//
// Zero NetworkInfo has zero current epoch.

View file

@ -7,6 +7,7 @@ import (
"github.com/nspcc-dev/neofs-api-go/v2/netmap"
. "github.com/nspcc-dev/neofs-sdk-go/netmap"
netmaptest "github.com/nspcc-dev/neofs-sdk-go/netmap/test"
"github.com/stretchr/testify/require"
)
@ -62,16 +63,16 @@ func TestNetworkInfo_MsPerBlock(t *testing.T) {
}
func testConfigValue(t *testing.T,
getter func(x NetworkInfo) interface{},
setter func(x *NetworkInfo, val interface{}),
val1, val2 interface{},
v2Key string, v2Val func(val interface{}) []byte,
getter func(x NetworkInfo) any,
setter func(x *NetworkInfo, val any),
val1, val2 any,
v2Key string, v2Val func(val any) []byte,
) {
var x NetworkInfo
require.Zero(t, getter(x))
checkVal := func(exp interface{}) {
checkVal := func(exp any) {
require.EqualValues(t, exp, getter(x))
var m netmap.NetworkInfo
@ -98,10 +99,10 @@ func testConfigValue(t *testing.T,
func TestNetworkInfo_AuditFee(t *testing.T) {
testConfigValue(t,
func(x NetworkInfo) interface{} { return x.AuditFee() },
func(info *NetworkInfo, val interface{}) { info.SetAuditFee(val.(uint64)) },
func(x NetworkInfo) any { return x.AuditFee() },
func(info *NetworkInfo, val any) { info.SetAuditFee(val.(uint64)) },
uint64(1), uint64(2),
"AuditFee", func(val interface{}) []byte {
"AuditFee", func(val any) []byte {
data := make([]byte, 8)
binary.LittleEndian.PutUint64(data, val.(uint64))
return data
@ -111,10 +112,10 @@ func TestNetworkInfo_AuditFee(t *testing.T) {
func TestNetworkInfo_StoragePrice(t *testing.T) {
testConfigValue(t,
func(x NetworkInfo) interface{} { return x.StoragePrice() },
func(info *NetworkInfo, val interface{}) { info.SetStoragePrice(val.(uint64)) },
func(x NetworkInfo) any { return x.StoragePrice() },
func(info *NetworkInfo, val any) { info.SetStoragePrice(val.(uint64)) },
uint64(1), uint64(2),
"BasicIncomeRate", func(val interface{}) []byte {
"BasicIncomeRate", func(val any) []byte {
data := make([]byte, 8)
binary.LittleEndian.PutUint64(data, val.(uint64))
return data
@ -124,10 +125,10 @@ func TestNetworkInfo_StoragePrice(t *testing.T) {
func TestNetworkInfo_ContainerFee(t *testing.T) {
testConfigValue(t,
func(x NetworkInfo) interface{} { return x.ContainerFee() },
func(info *NetworkInfo, val interface{}) { info.SetContainerFee(val.(uint64)) },
func(x NetworkInfo) any { return x.ContainerFee() },
func(info *NetworkInfo, val any) { info.SetContainerFee(val.(uint64)) },
uint64(1), uint64(2),
"ContainerFee", func(val interface{}) []byte {
"ContainerFee", func(val any) []byte {
data := make([]byte, 8)
binary.LittleEndian.PutUint64(data, val.(uint64))
return data
@ -137,10 +138,10 @@ func TestNetworkInfo_ContainerFee(t *testing.T) {
func TestNetworkInfo_NamedContainerFee(t *testing.T) {
testConfigValue(t,
func(x NetworkInfo) interface{} { return x.NamedContainerFee() },
func(info *NetworkInfo, val interface{}) { info.SetNamedContainerFee(val.(uint64)) },
func(x NetworkInfo) any { return x.NamedContainerFee() },
func(info *NetworkInfo, val any) { info.SetNamedContainerFee(val.(uint64)) },
uint64(1), uint64(2),
"ContainerAliasFee", func(val interface{}) []byte {
"ContainerAliasFee", func(val any) []byte {
data := make([]byte, 8)
binary.LittleEndian.PutUint64(data, val.(uint64))
return data
@ -150,10 +151,10 @@ func TestNetworkInfo_NamedContainerFee(t *testing.T) {
func TestNetworkInfo_EigenTrustAlpha(t *testing.T) {
testConfigValue(t,
func(x NetworkInfo) interface{} { return x.EigenTrustAlpha() },
func(info *NetworkInfo, val interface{}) { info.SetEigenTrustAlpha(val.(float64)) },
func(x NetworkInfo) any { return x.EigenTrustAlpha() },
func(info *NetworkInfo, val any) { info.SetEigenTrustAlpha(val.(float64)) },
0.1, 0.2,
"EigenTrustAlpha", func(val interface{}) []byte {
"EigenTrustAlpha", func(val any) []byte {
data := make([]byte, 8)
binary.LittleEndian.PutUint64(data, math.Float64bits(val.(float64)))
return data
@ -163,10 +164,10 @@ func TestNetworkInfo_EigenTrustAlpha(t *testing.T) {
func TestNetworkInfo_NumberOfEigenTrustIterations(t *testing.T) {
testConfigValue(t,
func(x NetworkInfo) interface{} { return x.NumberOfEigenTrustIterations() },
func(info *NetworkInfo, val interface{}) { info.SetNumberOfEigenTrustIterations(val.(uint64)) },
func(x NetworkInfo) any { return x.NumberOfEigenTrustIterations() },
func(info *NetworkInfo, val any) { info.SetNumberOfEigenTrustIterations(val.(uint64)) },
uint64(1), uint64(2),
"EigenTrustIterations", func(val interface{}) []byte {
"EigenTrustIterations", func(val any) []byte {
data := make([]byte, 8)
binary.LittleEndian.PutUint64(data, val.(uint64))
return data
@ -176,10 +177,10 @@ func TestNetworkInfo_NumberOfEigenTrustIterations(t *testing.T) {
func TestNetworkInfo_IRCandidateFee(t *testing.T) {
testConfigValue(t,
func(x NetworkInfo) interface{} { return x.IRCandidateFee() },
func(info *NetworkInfo, val interface{}) { info.SetIRCandidateFee(val.(uint64)) },
func(x NetworkInfo) any { return x.IRCandidateFee() },
func(info *NetworkInfo, val any) { info.SetIRCandidateFee(val.(uint64)) },
uint64(1), uint64(2),
"InnerRingCandidateFee", func(val interface{}) []byte {
"InnerRingCandidateFee", func(val any) []byte {
data := make([]byte, 8)
binary.LittleEndian.PutUint64(data, val.(uint64))
return data
@ -189,10 +190,10 @@ func TestNetworkInfo_IRCandidateFee(t *testing.T) {
func TestNetworkInfo_MaxObjectSize(t *testing.T) {
testConfigValue(t,
func(x NetworkInfo) interface{} { return x.MaxObjectSize() },
func(info *NetworkInfo, val interface{}) { info.SetMaxObjectSize(val.(uint64)) },
func(x NetworkInfo) any { return x.MaxObjectSize() },
func(info *NetworkInfo, val any) { info.SetMaxObjectSize(val.(uint64)) },
uint64(1), uint64(2),
"MaxObjectSize", func(val interface{}) []byte {
"MaxObjectSize", func(val any) []byte {
data := make([]byte, 8)
binary.LittleEndian.PutUint64(data, val.(uint64))
return data
@ -202,10 +203,10 @@ func TestNetworkInfo_MaxObjectSize(t *testing.T) {
func TestNetworkInfo_WithdrawalFee(t *testing.T) {
testConfigValue(t,
func(x NetworkInfo) interface{} { return x.WithdrawalFee() },
func(info *NetworkInfo, val interface{}) { info.SetWithdrawalFee(val.(uint64)) },
func(x NetworkInfo) any { return x.WithdrawalFee() },
func(info *NetworkInfo, val any) { info.SetWithdrawalFee(val.(uint64)) },
uint64(1), uint64(2),
"WithdrawFee", func(val interface{}) []byte {
"WithdrawFee", func(val any) []byte {
data := make([]byte, 8)
binary.LittleEndian.PutUint64(data, val.(uint64))
return data
@ -215,14 +216,14 @@ func TestNetworkInfo_WithdrawalFee(t *testing.T) {
func TestNetworkInfo_HomomorphicHashingDisabled(t *testing.T) {
testConfigValue(t,
func(x NetworkInfo) interface{} { return x.HomomorphicHashingDisabled() },
func(info *NetworkInfo, val interface{}) {
func(x NetworkInfo) any { return x.HomomorphicHashingDisabled() },
func(info *NetworkInfo, val any) {
if val.(bool) {
info.DisableHomomorphicHashing()
}
},
true, true, // it is impossible to enable hashing
"HomomorphicHashingDisabled", func(val interface{}) []byte {
"HomomorphicHashingDisabled", func(val any) []byte {
data := make([]byte, 1)
if val.(bool) {
@ -236,14 +237,14 @@ func TestNetworkInfo_HomomorphicHashingDisabled(t *testing.T) {
func TestNetworkInfo_MaintenanceModeAllowed(t *testing.T) {
testConfigValue(t,
func(x NetworkInfo) interface{} { return x.MaintenanceModeAllowed() },
func(info *NetworkInfo, val interface{}) {
func(x NetworkInfo) any { return x.MaintenanceModeAllowed() },
func(info *NetworkInfo, val any) {
if val.(bool) {
info.AllowMaintenanceMode()
}
},
true, true,
"MaintenanceModeAllowed", func(val interface{}) []byte {
"MaintenanceModeAllowed", func(val any) []byte {
if val.(bool) {
return []byte{1}
}
@ -251,3 +252,12 @@ func TestNetworkInfo_MaintenanceModeAllowed(t *testing.T) {
},
)
}
func TestNetworkInfo_Marshal(t *testing.T) {
v := netmaptest.NetworkInfo()
var v2 NetworkInfo
require.NoError(t, v2.Unmarshal(v.Marshal()))
require.Equal(t, v, v2)
}

View file

@ -9,9 +9,7 @@ import (
"github.com/nspcc-dev/hrw"
"github.com/nspcc-dev/neofs-api-go/v2/netmap"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
)
// NodeInfo groups information about NeoFS storage node which is reflected
@ -53,8 +51,6 @@ func (x *NodeInfo) readFromV2(m netmap.NodeInfo, checkFieldPresence bool) error
return fmt.Errorf("duplicated attbiuted %s", key)
}
const subnetPrefix = "__NEOFS__SUBNET_"
switch {
case key == attrCapacity:
_, err = strconv.ParseUint(attributes[i].GetValue(), 10, 64)
@ -67,17 +63,6 @@ func (x *NodeInfo) readFromV2(m netmap.NodeInfo, checkFieldPresence bool) error
if err != nil {
return fmt.Errorf("invalid %s attribute: %w", attrPrice, err)
}
case strings.HasPrefix(key, subnetPrefix):
var id subnetid.ID
err = id.DecodeString(strings.TrimPrefix(key, subnetPrefix))
if err != nil {
return fmt.Errorf("invalid key to the subnet attribute %s: %w", key, err)
}
if val := attributes[i].GetValue(); val != "True" && val != "False" {
return fmt.Errorf("invalid value of the subnet attribute %s: %w", val, err)
}
default:
if attributes[i].GetValue() == "" {
return fmt.Errorf("empty value of the attribute %s", key)
@ -478,81 +463,6 @@ func (x *NodeInfo) SortAttributes() {
x.m.SetAttributes(as)
}
// EnterSubnet writes storage node's intention to enter the given subnet.
//
// Zero NodeInfo belongs to zero subnet.
func (x *NodeInfo) EnterSubnet(id subnetid.ID) {
x.changeSubnet(id, true)
}
// ExitSubnet writes storage node's intention to exit the given subnet.
func (x *NodeInfo) ExitSubnet(id subnetid.ID) {
x.changeSubnet(id, false)
}
func (x *NodeInfo) changeSubnet(id subnetid.ID, isMember bool) {
var (
idv2 refs.SubnetID
info netmap.NodeSubnetInfo
)
id.WriteToV2(&idv2)
info.SetID(&idv2)
info.SetEntryFlag(isMember)
netmap.WriteSubnetInfo(&x.m, info)
}
// ErrRemoveSubnet is returned when a node needs to leave the subnet.
var ErrRemoveSubnet = netmap.ErrRemoveSubnet
// IterateSubnets iterates over all subnets the node belongs to and passes the IDs to f.
// Handler MUST NOT be nil.
//
// If f returns ErrRemoveSubnet, then removes subnet entry. Note that this leads to an
// instant mutation of NodeInfo. Breaks on any other non-nil error and returns it.
//
// Returns an error if subnet incorrectly enabled/disabled.
// Returns an error if the node is not included to any subnet by the end of the loop.
//
// See also EnterSubnet, ExitSubnet.
func (x NodeInfo) IterateSubnets(f func(subnetid.ID) error) error {
var id subnetid.ID
return netmap.IterateSubnets(&x.m, func(idv2 refs.SubnetID) error {
err := id.ReadFromV2(idv2)
if err != nil {
return fmt.Errorf("invalid subnet: %w", err)
}
err = f(id)
if errors.Is(err, ErrRemoveSubnet) {
return netmap.ErrRemoveSubnet
}
return err
})
}
var errAbortSubnetIter = errors.New("abort subnet iterator")
// BelongsToSubnet is a helper function over the IterateSubnets method which
// checks whether a node belongs to a subnet.
//
// Zero NodeInfo belongs to zero subnet only.
func BelongsToSubnet(node NodeInfo, id subnetid.ID) bool {
err := node.IterateSubnets(func(id_ subnetid.ID) error {
if id.Equals(id_) {
return errAbortSubnetIter
}
return nil
})
return errors.Is(err, errAbortSubnetIter)
}
// SetOffline sets the state of the node to "offline". When a node updates
// information about itself in the network map, this action is interpreted as
// an intention to leave the network.

View file

@ -1,3 +1,4 @@
package parser
//go:generate antlr4 -Dlanguage=Go -visitor QueryLexer.g4 Query.g4
// ANTLR can be downloaded from https://www.antlr.org/download/antlr-4.11.1-complete.jar
//go:generate java -Xmx500M -cp "./antlr-4.11.1-complete.jar:$CLASSPATH" org.antlr.v4.Tool -Dlanguage=Go -visitor QueryLexer.g4 Query.g4

View file

@ -1,8 +1,8 @@
// Code generated from Query.g4 by ANTLR 4.10.1. DO NOT EDIT.
// Code generated from java-escape by ANTLR 4.11.1. DO NOT EDIT.
package parser // Query
import "github.com/antlr/antlr4/runtime/Go/antlr"
import "github.com/antlr/antlr4/runtime/Go/antlr/v4"
// BaseQueryListener is a complete listener for a parse tree produced by Query.
type BaseQueryListener struct{}

View file

@ -1,65 +1,65 @@
// Code generated from Query.g4 by ANTLR 4.10.1. DO NOT EDIT.
// Code generated from java-escape by ANTLR 4.11.1. DO NOT EDIT.
package parser // Query
import "github.com/antlr/antlr4/runtime/Go/antlr"
import "github.com/antlr/antlr4/runtime/Go/antlr/v4"
type BaseQueryVisitor struct {
*antlr.BaseParseTreeVisitor
}
func (v *BaseQueryVisitor) VisitPolicy(ctx *PolicyContext) interface{} {
func (v *BaseQueryVisitor) VisitPolicy(ctx *PolicyContext) any {
return v.VisitChildren(ctx)
}
func (v *BaseQueryVisitor) VisitRepStmt(ctx *RepStmtContext) interface{} {
func (v *BaseQueryVisitor) VisitRepStmt(ctx *RepStmtContext) any {
return v.VisitChildren(ctx)
}
func (v *BaseQueryVisitor) VisitCbfStmt(ctx *CbfStmtContext) interface{} {
func (v *BaseQueryVisitor) VisitCbfStmt(ctx *CbfStmtContext) any {
return v.VisitChildren(ctx)
}
func (v *BaseQueryVisitor) VisitSelectStmt(ctx *SelectStmtContext) interface{} {
func (v *BaseQueryVisitor) VisitSelectStmt(ctx *SelectStmtContext) any {
return v.VisitChildren(ctx)
}
func (v *BaseQueryVisitor) VisitClause(ctx *ClauseContext) interface{} {
func (v *BaseQueryVisitor) VisitClause(ctx *ClauseContext) any {
return v.VisitChildren(ctx)
}
func (v *BaseQueryVisitor) VisitFilterExpr(ctx *FilterExprContext) interface{} {
func (v *BaseQueryVisitor) VisitFilterExpr(ctx *FilterExprContext) any {
return v.VisitChildren(ctx)
}
func (v *BaseQueryVisitor) VisitFilterStmt(ctx *FilterStmtContext) interface{} {
func (v *BaseQueryVisitor) VisitFilterStmt(ctx *FilterStmtContext) any {
return v.VisitChildren(ctx)
}
func (v *BaseQueryVisitor) VisitExpr(ctx *ExprContext) interface{} {
func (v *BaseQueryVisitor) VisitExpr(ctx *ExprContext) any {
return v.VisitChildren(ctx)
}
func (v *BaseQueryVisitor) VisitFilterKey(ctx *FilterKeyContext) interface{} {
func (v *BaseQueryVisitor) VisitFilterKey(ctx *FilterKeyContext) any {
return v.VisitChildren(ctx)
}
func (v *BaseQueryVisitor) VisitFilterValue(ctx *FilterValueContext) interface{} {
func (v *BaseQueryVisitor) VisitFilterValue(ctx *FilterValueContext) any {
return v.VisitChildren(ctx)
}
func (v *BaseQueryVisitor) VisitNumber(ctx *NumberContext) interface{} {
func (v *BaseQueryVisitor) VisitNumber(ctx *NumberContext) any {
return v.VisitChildren(ctx)
}
func (v *BaseQueryVisitor) VisitKeyword(ctx *KeywordContext) interface{} {
func (v *BaseQueryVisitor) VisitKeyword(ctx *KeywordContext) any {
return v.VisitChildren(ctx)
}
func (v *BaseQueryVisitor) VisitIdent(ctx *IdentContext) interface{} {
func (v *BaseQueryVisitor) VisitIdent(ctx *IdentContext) any {
return v.VisitChildren(ctx)
}
func (v *BaseQueryVisitor) VisitIdentWC(ctx *IdentWCContext) interface{} {
func (v *BaseQueryVisitor) VisitIdentWC(ctx *IdentWCContext) any {
return v.VisitChildren(ctx)
}

View file

@ -1,4 +1,4 @@
// Code generated from QueryLexer.g4 by ANTLR 4.10.1. DO NOT EDIT.
// Code generated from java-escape by ANTLR 4.11.1. DO NOT EDIT.
package parser
@ -7,7 +7,7 @@ import (
"sync"
"unicode"
"github.com/antlr/antlr4/runtime/Go/antlr"
"github.com/antlr/antlr4/runtime/Go/antlr/v4"
)
// Suppress unused import error

View file

@ -1,8 +1,8 @@
// Code generated from Query.g4 by ANTLR 4.10.1. DO NOT EDIT.
// Code generated from java-escape by ANTLR 4.11.1. DO NOT EDIT.
package parser // Query
import "github.com/antlr/antlr4/runtime/Go/antlr"
import "github.com/antlr/antlr4/runtime/Go/antlr/v4"
// QueryListener is a complete listener for a parse tree produced by Query.
type QueryListener interface {

View file

@ -1,4 +1,4 @@
// Code generated from Query.g4 by ANTLR 4.10.1. DO NOT EDIT.
// Code generated from java-escape by ANTLR 4.11.1. DO NOT EDIT.
package parser // Query
@ -7,7 +7,7 @@ import (
"strconv"
"sync"
"github.com/antlr/antlr4/runtime/Go/antlr"
"github.com/antlr/antlr4/runtime/Go/antlr/v4"
)
// Suppress unused import errors
@ -133,7 +133,7 @@ func NewQuery(input antlr.TokenStream) *Query {
this.RuleNames = staticData.ruleNames
this.LiteralNames = staticData.literalNames
this.SymbolicNames = staticData.symbolicNames
this.GrammarFileName = "Query.g4"
this.GrammarFileName = "java-escape"
return this
}
@ -383,7 +383,7 @@ func (s *PolicyContext) ExitRule(listener antlr.ParseTreeListener) {
}
}
func (s *PolicyContext) Accept(visitor antlr.ParseTreeVisitor) interface{} {
func (s *PolicyContext) Accept(visitor antlr.ParseTreeVisitor) any {
switch t := visitor.(type) {
case QueryVisitor:
return t.VisitPolicy(s)
@ -587,7 +587,7 @@ func (s *RepStmtContext) ExitRule(listener antlr.ParseTreeListener) {
}
}
func (s *RepStmtContext) Accept(visitor antlr.ParseTreeVisitor) interface{} {
func (s *RepStmtContext) Accept(visitor antlr.ParseTreeVisitor) any {
switch t := visitor.(type) {
case QueryVisitor:
return t.VisitRepStmt(s)
@ -732,7 +732,7 @@ func (s *CbfStmtContext) ExitRule(listener antlr.ParseTreeListener) {
}
}
func (s *CbfStmtContext) Accept(visitor antlr.ParseTreeVisitor) interface{} {
func (s *CbfStmtContext) Accept(visitor antlr.ParseTreeVisitor) any {
switch t := visitor.(type) {
case QueryVisitor:
return t.VisitCbfStmt(s)
@ -976,7 +976,7 @@ func (s *SelectStmtContext) ExitRule(listener antlr.ParseTreeListener) {
}
}
func (s *SelectStmtContext) Accept(visitor antlr.ParseTreeVisitor) interface{} {
func (s *SelectStmtContext) Accept(visitor antlr.ParseTreeVisitor) any {
switch t := visitor.(type) {
case QueryVisitor:
return t.VisitSelectStmt(s)
@ -1150,7 +1150,7 @@ func (s *ClauseContext) ExitRule(listener antlr.ParseTreeListener) {
}
}
func (s *ClauseContext) Accept(visitor antlr.ParseTreeVisitor) interface{} {
func (s *ClauseContext) Accept(visitor antlr.ParseTreeVisitor) any {
switch t := visitor.(type) {
case QueryVisitor:
return t.VisitClause(s)
@ -1375,7 +1375,7 @@ func (s *FilterExprContext) ExitRule(listener antlr.ParseTreeListener) {
}
}
func (s *FilterExprContext) Accept(visitor antlr.ParseTreeVisitor) interface{} {
func (s *FilterExprContext) Accept(visitor antlr.ParseTreeVisitor) any {
switch t := visitor.(type) {
case QueryVisitor:
return t.VisitFilterExpr(s)
@ -1643,7 +1643,7 @@ func (s *FilterStmtContext) ExitRule(listener antlr.ParseTreeListener) {
}
}
func (s *FilterStmtContext) Accept(visitor antlr.ParseTreeVisitor) interface{} {
func (s *FilterStmtContext) Accept(visitor antlr.ParseTreeVisitor) any {
switch t := visitor.(type) {
case QueryVisitor:
return t.VisitFilterStmt(s)
@ -1850,7 +1850,7 @@ func (s *ExprContext) ExitRule(listener antlr.ParseTreeListener) {
}
}
func (s *ExprContext) Accept(visitor antlr.ParseTreeVisitor) interface{} {
func (s *ExprContext) Accept(visitor antlr.ParseTreeVisitor) any {
switch t := visitor.(type) {
case QueryVisitor:
return t.VisitExpr(s)
@ -2007,7 +2007,7 @@ func (s *FilterKeyContext) ExitRule(listener antlr.ParseTreeListener) {
}
}
func (s *FilterKeyContext) Accept(visitor antlr.ParseTreeVisitor) interface{} {
func (s *FilterKeyContext) Accept(visitor antlr.ParseTreeVisitor) any {
switch t := visitor.(type) {
case QueryVisitor:
return t.VisitFilterKey(s)
@ -2159,7 +2159,7 @@ func (s *FilterValueContext) ExitRule(listener antlr.ParseTreeListener) {
}
}
func (s *FilterValueContext) Accept(visitor antlr.ParseTreeVisitor) interface{} {
func (s *FilterValueContext) Accept(visitor antlr.ParseTreeVisitor) any {
switch t := visitor.(type) {
case QueryVisitor:
return t.VisitFilterValue(s)
@ -2290,7 +2290,7 @@ func (s *NumberContext) ExitRule(listener antlr.ParseTreeListener) {
}
}
func (s *NumberContext) Accept(visitor antlr.ParseTreeVisitor) interface{} {
func (s *NumberContext) Accept(visitor antlr.ParseTreeVisitor) any {
switch t := visitor.(type) {
case QueryVisitor:
return t.VisitNumber(s)
@ -2422,7 +2422,7 @@ func (s *KeywordContext) ExitRule(listener antlr.ParseTreeListener) {
}
}
func (s *KeywordContext) Accept(visitor antlr.ParseTreeVisitor) interface{} {
func (s *KeywordContext) Accept(visitor antlr.ParseTreeVisitor) any {
switch t := visitor.(type) {
case QueryVisitor:
return t.VisitKeyword(s)
@ -2461,7 +2461,7 @@ func (p *Query) Keyword() (localctx IKeywordContext) {
p.SetState(119)
_la = p.GetTokenStream().LA(1)
if !(((_la)&-(0x1f+1)) == 0 && ((1<<uint(_la))&((1<<QueryREP)|(1<<QueryIN)|(1<<QueryAS)|(1<<QuerySELECT)|(1<<QueryFROM)|(1<<QueryFILTER))) != 0) {
if !((int64(_la) & ^0x3f) == 0 && ((int64(1)<<_la)&1904) != 0) {
p.GetErrorHandler().RecoverInline(p)
} else {
p.GetErrorHandler().ReportMatch(p)
@ -2550,7 +2550,7 @@ func (s *IdentContext) ExitRule(listener antlr.ParseTreeListener) {
}
}
func (s *IdentContext) Accept(visitor antlr.ParseTreeVisitor) interface{} {
func (s *IdentContext) Accept(visitor antlr.ParseTreeVisitor) any {
switch t := visitor.(type) {
case QueryVisitor:
return t.VisitIdent(s)
@ -2686,7 +2686,7 @@ func (s *IdentWCContext) ExitRule(listener antlr.ParseTreeListener) {
}
}
func (s *IdentWCContext) Accept(visitor antlr.ParseTreeVisitor) interface{} {
func (s *IdentWCContext) Accept(visitor antlr.ParseTreeVisitor) any {
switch t := visitor.(type) {
case QueryVisitor:
return t.VisitIdentWC(s)

View file

@ -1,52 +1,52 @@
// Code generated from Query.g4 by ANTLR 4.10.1. DO NOT EDIT.
// Code generated from java-escape by ANTLR 4.11.1. DO NOT EDIT.
package parser // Query
import "github.com/antlr/antlr4/runtime/Go/antlr"
import "github.com/antlr/antlr4/runtime/Go/antlr/v4"
// A complete Visitor for a parse tree produced by Query.
type QueryVisitor interface {
antlr.ParseTreeVisitor
// Visit a parse tree produced by Query#policy.
VisitPolicy(ctx *PolicyContext) interface{}
VisitPolicy(ctx *PolicyContext) any
// Visit a parse tree produced by Query#repStmt.
VisitRepStmt(ctx *RepStmtContext) interface{}
VisitRepStmt(ctx *RepStmtContext) any
// Visit a parse tree produced by Query#cbfStmt.
VisitCbfStmt(ctx *CbfStmtContext) interface{}
VisitCbfStmt(ctx *CbfStmtContext) any
// Visit a parse tree produced by Query#selectStmt.
VisitSelectStmt(ctx *SelectStmtContext) interface{}
VisitSelectStmt(ctx *SelectStmtContext) any
// Visit a parse tree produced by Query#clause.
VisitClause(ctx *ClauseContext) interface{}
VisitClause(ctx *ClauseContext) any
// Visit a parse tree produced by Query#filterExpr.
VisitFilterExpr(ctx *FilterExprContext) interface{}
VisitFilterExpr(ctx *FilterExprContext) any
// Visit a parse tree produced by Query#filterStmt.
VisitFilterStmt(ctx *FilterStmtContext) interface{}
VisitFilterStmt(ctx *FilterStmtContext) any
// Visit a parse tree produced by Query#expr.
VisitExpr(ctx *ExprContext) interface{}
VisitExpr(ctx *ExprContext) any
// Visit a parse tree produced by Query#filterKey.
VisitFilterKey(ctx *FilterKeyContext) interface{}
VisitFilterKey(ctx *FilterKeyContext) any
// Visit a parse tree produced by Query#filterValue.
VisitFilterValue(ctx *FilterValueContext) interface{}
VisitFilterValue(ctx *FilterValueContext) any
// Visit a parse tree produced by Query#number.
VisitNumber(ctx *NumberContext) interface{}
VisitNumber(ctx *NumberContext) any
// Visit a parse tree produced by Query#keyword.
VisitKeyword(ctx *KeywordContext) interface{}
VisitKeyword(ctx *KeywordContext) any
// Visit a parse tree produced by Query#ident.
VisitIdent(ctx *IdentContext) interface{}
VisitIdent(ctx *IdentContext) any
// Visit a parse tree produced by Query#identWC.
VisitIdentWC(ctx *IdentWCContext) interface{}
VisitIdentWC(ctx *IdentWCContext) any
}

View file

@ -7,11 +7,9 @@ import (
"strconv"
"strings"
"github.com/antlr/antlr4/runtime/Go/antlr"
"github.com/antlr/antlr4/runtime/Go/antlr/v4"
"github.com/nspcc-dev/neofs-api-go/v2/netmap"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
"github.com/nspcc-dev/neofs-sdk-go/netmap/parser"
subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
)
// PlacementPolicy declares policy to store objects in the NeoFS container.
@ -25,8 +23,6 @@ import (
type PlacementPolicy struct {
backupFactor uint32
subnet subnetid.ID
filters []netmap.Filter
selectors []netmap.Selector
@ -40,16 +36,6 @@ func (p *PlacementPolicy) readFromV2(m netmap.PlacementPolicy, checkFieldPresenc
return errors.New("missing replicas")
}
subnetV2 := m.GetSubnetID()
if subnetV2 != nil {
err := p.subnet.ReadFromV2(*subnetV2)
if err != nil {
return fmt.Errorf("invalid subnet: %w", err)
}
} else {
p.subnet = subnetid.ID{}
}
p.backupFactor = m.GetContainerBackupFactor()
p.selectors = m.GetSelectors()
p.filters = m.GetFilters()
@ -123,29 +109,12 @@ func (p *PlacementPolicy) ReadFromV2(m netmap.PlacementPolicy) error {
//
// See also ReadFromV2.
func (p PlacementPolicy) WriteToV2(m *netmap.PlacementPolicy) {
var subnetV2 refs.SubnetID
p.subnet.WriteToV2(&subnetV2)
m.SetContainerBackupFactor(p.backupFactor)
m.SetSubnetID(&subnetV2)
m.SetFilters(p.filters)
m.SetSelectors(p.selectors)
m.SetReplicas(p.replicas)
}
// RestrictSubnet sets a rule to select nodes from the given subnet only.
// By default, nodes from zero subnet are selected (whole network map).
func (p *PlacementPolicy) RestrictSubnet(subnet subnetid.ID) {
p.subnet = subnet
}
// Subnet returns subnet set using RestrictSubnet.
//
// Zero PlacementPolicy returns zero subnet meaning unlimited.
func (p PlacementPolicy) Subnet() subnetid.ID {
return p.subnet
}
// ReplicaDescriptor replica descriptor characterizes replicas of objects from
// the subset selected by a particular Selector.
type ReplicaDescriptor struct {
@ -601,17 +570,17 @@ type policyVisitor struct {
antlr.DefaultErrorListener
}
func (p *policyVisitor) SyntaxError(_ antlr.Recognizer, _ interface{}, line, column int, msg string, _ antlr.RecognitionException) {
func (p *policyVisitor) SyntaxError(_ antlr.Recognizer, _ any, line, column int, msg string, _ antlr.RecognitionException) {
p.reportError(fmt.Errorf("%w: line %d:%d %s", errSyntaxError, line, column, msg))
}
func (p *policyVisitor) reportError(err error) interface{} {
func (p *policyVisitor) reportError(err error) any {
p.errors = append(p.errors, err)
return nil
}
// VisitPolicy implements parser.QueryVisitor interface.
func (p *policyVisitor) VisitPolicy(ctx *parser.PolicyContext) interface{} {
func (p *policyVisitor) VisitPolicy(ctx *parser.PolicyContext) any {
if len(p.errors) != 0 {
return nil
}
@ -659,7 +628,7 @@ func (p *policyVisitor) VisitPolicy(ctx *parser.PolicyContext) interface{} {
return pl
}
func (p *policyVisitor) VisitCbfStmt(ctx *parser.CbfStmtContext) interface{} {
func (p *policyVisitor) VisitCbfStmt(ctx *parser.CbfStmtContext) any {
cbf, err := strconv.ParseUint(ctx.GetBackupFactor().GetText(), 10, 32)
if err != nil {
return p.reportError(errInvalidNumber)
@ -669,7 +638,7 @@ func (p *policyVisitor) VisitCbfStmt(ctx *parser.CbfStmtContext) interface{} {
}
// VisitRepStmt implements parser.QueryVisitor interface.
func (p *policyVisitor) VisitRepStmt(ctx *parser.RepStmtContext) interface{} {
func (p *policyVisitor) VisitRepStmt(ctx *parser.RepStmtContext) any {
num, err := strconv.ParseUint(ctx.GetCount().GetText(), 10, 32)
if err != nil {
return p.reportError(errInvalidNumber)
@ -686,7 +655,7 @@ func (p *policyVisitor) VisitRepStmt(ctx *parser.RepStmtContext) interface{} {
}
// VisitSelectStmt implements parser.QueryVisitor interface.
func (p *policyVisitor) VisitSelectStmt(ctx *parser.SelectStmtContext) interface{} {
func (p *policyVisitor) VisitSelectStmt(ctx *parser.SelectStmtContext) any {
res, err := strconv.ParseUint(ctx.GetCount().GetText(), 10, 32)
if err != nil {
return p.reportError(errInvalidNumber)
@ -712,13 +681,13 @@ func (p *policyVisitor) VisitSelectStmt(ctx *parser.SelectStmtContext) interface
}
// VisitFilterStmt implements parser.QueryVisitor interface.
func (p *policyVisitor) VisitFilterStmt(ctx *parser.FilterStmtContext) interface{} {
func (p *policyVisitor) VisitFilterStmt(ctx *parser.FilterStmtContext) any {
f := p.VisitFilterExpr(ctx.GetExpr().(*parser.FilterExprContext)).(*netmap.Filter)
f.SetName(ctx.GetName().GetText())
return f
}
func (p *policyVisitor) VisitFilterExpr(ctx *parser.FilterExprContext) interface{} {
func (p *policyVisitor) VisitFilterExpr(ctx *parser.FilterExprContext) any {
if eCtx := ctx.Expr(); eCtx != nil {
return eCtx.Accept(p)
}
@ -747,7 +716,7 @@ func (p *policyVisitor) VisitFilterExpr(ctx *parser.FilterExprContext) interface
}
// VisitFilterKey implements parser.QueryVisitor interface.
func (p *policyVisitor) VisitFilterKey(ctx *parser.FilterKeyContext) interface{} {
func (p *policyVisitor) VisitFilterKey(ctx *parser.FilterKeyContext) any {
if id := ctx.Ident(); id != nil {
return id.GetText()
}
@ -756,7 +725,7 @@ func (p *policyVisitor) VisitFilterKey(ctx *parser.FilterKeyContext) interface{}
return str[1 : len(str)-1]
}
func (p *policyVisitor) VisitFilterValue(ctx *parser.FilterValueContext) interface{} {
func (p *policyVisitor) VisitFilterValue(ctx *parser.FilterValueContext) any {
if id := ctx.Ident(); id != nil {
return id.GetText()
}
@ -770,7 +739,7 @@ func (p *policyVisitor) VisitFilterValue(ctx *parser.FilterValueContext) interfa
}
// VisitExpr implements parser.QueryVisitor interface.
func (p *policyVisitor) VisitExpr(ctx *parser.ExprContext) interface{} {
func (p *policyVisitor) VisitExpr(ctx *parser.ExprContext) any {
f := new(netmap.Filter)
if flt := ctx.GetFilter(); flt != nil {
f.SetName(flt.GetText())

View file

@ -28,12 +28,10 @@ FILTER City EQ SPB AND SSD EQ true OR City EQ SPB AND Rating GE 5 AS SPBSSD`,
var p PlacementPolicy
for _, testCase := range testCases {
require.NoError(t, p.DecodeString(testCase))
var b strings.Builder
require.NoError(t, p.WriteStringTo(&b))
require.Equal(t, testCase, b.String())
}

View file

@ -6,7 +6,6 @@ import (
"github.com/nspcc-dev/hrw"
"github.com/nspcc-dev/neofs-api-go/v2/netmap"
subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
)
// processSelectors processes selectors and returns error is any of them is invalid.
@ -59,7 +58,7 @@ func calcBucketWeight(ns nodes, a aggregator, wf weightFunc) float64 {
// Last argument specifies if more buckets can be used to fulfill CBF.
func (c *context) getSelection(p PlacementPolicy, s netmap.Selector) ([]nodes, error) {
bucketCount, nodesInBucket := calcNodesCount(s)
buckets := c.getSelectionBase(p.subnet, s)
buckets := c.getSelectionBase(s)
if len(buckets) < bucketCount {
return nil, fmt.Errorf("%w: '%s'", errNotEnoughNodes, s.GetName())
@ -132,7 +131,7 @@ type nodeAttrPair struct {
// getSelectionBase returns nodes grouped by selector attribute.
// It it guaranteed that each pair will contain at least one node.
func (c *context) getSelectionBase(subnetID subnetid.ID, s netmap.Selector) []nodeAttrPair {
func (c *context) getSelectionBase(s netmap.Selector) []nodeAttrPair {
fName := s.GetFilter()
f := c.processedFilters[fName]
isMain := fName == mainFilterName
@ -141,10 +140,6 @@ func (c *context) getSelectionBase(subnetID subnetid.ID, s netmap.Selector) []no
attr := s.GetAttribute()
for i := range c.netMap.nodes {
// TODO(fyrchik): make `BelongsToSubnet` to accept pointer
if !BelongsToSubnet(c.netMap.nodes[i], subnetID) {
continue
}
if isMain || c.match(f, c.netMap.nodes[i]) {
if attr == "" {
// Default attribute is transparent identifier which is different for every node.

View file

@ -9,6 +9,7 @@ import (
"github.com/nspcc-dev/hrw"
"github.com/nspcc-dev/neofs-api-go/v2/netmap"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"github.com/stretchr/testify/require"
)
@ -129,7 +130,7 @@ func BenchmarkPolicyHRWType(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := nm.ContainerNodes(p, []byte{1})
_, err := nm.ContainerNodes(p, cid.ID{1})
if err != nil {
b.Fatal()
}
@ -173,7 +174,7 @@ func TestPlacementPolicy_DeterministicOrder(t *testing.T) {
nm.SetNodes(nodeList)
getIndices := func(t *testing.T) (uint64, uint64) {
v, err := nm.ContainerNodes(p, []byte{1})
v, err := nm.ContainerNodes(p, cid.ID{1})
require.NoError(t, err)
nss := make([]nodes, len(v))

View file

@ -1,126 +0,0 @@
package netmap_test
import (
"testing"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id"
"github.com/stretchr/testify/require"
)
func TestNodeInfoSubnets(t *testing.T) {
t.Run("enter subnet", func(t *testing.T) {
var id subnetid.ID
id.SetNumeric(13)
var node netmap.NodeInfo
node.EnterSubnet(id)
mIDs := make(map[string]struct{})
err := node.IterateSubnets(func(id subnetid.ID) error {
mIDs[id.String()] = struct{}{}
return nil
})
require.NoError(t, err)
_, ok := mIDs[id.String()]
require.True(t, ok)
})
t.Run("iterate with removal", func(t *testing.T) {
t.Run("not last", func(t *testing.T) {
var id, idrm subnetid.ID
id.SetNumeric(13)
idrm.SetNumeric(23)
var node netmap.NodeInfo
node.EnterSubnet(id)
node.EnterSubnet(idrm)
err := node.IterateSubnets(func(id subnetid.ID) error {
if subnetid.IsZero(id) || id.Equals(idrm) {
return netmap.ErrRemoveSubnet
}
return nil
})
require.NoError(t, err)
mIDs := make(map[string]struct{})
err = node.IterateSubnets(func(id subnetid.ID) error {
mIDs[id.String()] = struct{}{}
return nil
})
require.NoError(t, err)
var zeroID subnetid.ID
_, ok := mIDs[zeroID.String()]
require.False(t, ok)
_, ok = mIDs[idrm.String()]
require.False(t, ok)
_, ok = mIDs[id.String()]
require.True(t, ok)
})
t.Run("last", func(t *testing.T) {
var node netmap.NodeInfo
err := node.IterateSubnets(func(id subnetid.ID) error {
return netmap.ErrRemoveSubnet
})
require.Error(t, err)
})
})
}
func TestEnterSubnet(t *testing.T) {
var (
id subnetid.ID
node netmap.NodeInfo
)
require.True(t, netmap.BelongsToSubnet(node, id))
node.EnterSubnet(id)
require.True(t, netmap.BelongsToSubnet(node, id))
node.ExitSubnet(id)
require.False(t, netmap.BelongsToSubnet(node, id))
id.SetNumeric(10)
node.EnterSubnet(id)
require.True(t, netmap.BelongsToSubnet(node, id))
require.False(t, netmap.BelongsToSubnet(node, subnetid.ID{}))
node.ExitSubnet(id)
require.False(t, netmap.BelongsToSubnet(node, id))
require.False(t, netmap.BelongsToSubnet(node, subnetid.ID{}))
}
func TestBelongsToSubnet(t *testing.T) {
var id, idMiss, idZero subnetid.ID
id.SetNumeric(13)
idMiss.SetNumeric(23)
var node netmap.NodeInfo
node.EnterSubnet(id)
require.True(t, netmap.BelongsToSubnet(node, idZero))
require.True(t, netmap.BelongsToSubnet(node, id))
require.False(t, netmap.BelongsToSubnet(node, idMiss))
}

View file

@ -4,7 +4,6 @@ import (
"math/rand"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
subnetidtest "github.com/nspcc-dev/neofs-sdk-go/subnet/id/test"
)
func filter(withInner bool) (x netmap.Filter) {
@ -48,7 +47,6 @@ func PlacementPolicy() (p netmap.PlacementPolicy) {
p.AddFilters(Filter(), Filter())
p.AddReplicas(Replica(), Replica())
p.AddSelectors(Selector(), Selector())
p.RestrictSubnet(subnetidtest.ID())
return
}

View file

@ -26,7 +26,7 @@ type NNS struct {
nnsContract util.Uint160
invoker interface {
Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error)
Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error)
}
}

View file

@ -29,7 +29,7 @@ type testNeoClient struct {
err error
}
func (x *testNeoClient) Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) {
func (x *testNeoClient) Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) {
var domain string
require.Equal(x.t, x.expectedContract, contract)
@ -49,7 +49,7 @@ type brokenArrayStackItem struct {
stackitem.Item
}
func (x brokenArrayStackItem) Value() interface{} {
func (x brokenArrayStackItem) Value() any {
return 1
}

View file

@ -2,7 +2,6 @@ package object
import (
"bytes"
"crypto/ecdsa"
"crypto/sha256"
"errors"
"fmt"
@ -96,13 +95,13 @@ func VerifyID(obj *Object) error {
// CalculateAndSetSignature signs id with provided key and sets that signature to
// the object.
func CalculateAndSetSignature(key ecdsa.PrivateKey, obj *Object) error {
func CalculateAndSetSignature(signer neofscrypto.Signer, obj *Object) error {
oID, set := obj.ID()
if !set {
return errOIDNotSet
}
sig, err := oID.CalculateIDSignature(key)
sig, err := oID.CalculateIDSignature(signer)
if err != nil {
return err
}
@ -132,12 +131,12 @@ func (o *Object) VerifyIDSignature() bool {
}
// SetIDWithSignature sets object identifier and signature.
func SetIDWithSignature(key ecdsa.PrivateKey, obj *Object) error {
func SetIDWithSignature(signer neofscrypto.Signer, obj *Object) error {
if err := CalculateAndSetID(obj); err != nil {
return fmt.Errorf("could not set identifier: %w", err)
}
if err := CalculateAndSetSignature(key, obj); err != nil {
if err := CalculateAndSetSignature(signer, obj); err != nil {
return fmt.Errorf("could not set signature: %w", err)
}
@ -145,10 +144,10 @@ func SetIDWithSignature(key ecdsa.PrivateKey, obj *Object) error {
}
// SetVerificationFields calculates and sets all verification fields of the object.
func SetVerificationFields(key ecdsa.PrivateKey, obj *Object) error {
func SetVerificationFields(signer neofscrypto.Signer, obj *Object) error {
CalculateAndSetPayloadChecksum(obj)
return SetIDWithSignature(key, obj)
return SetIDWithSignature(signer, obj)
}
// CheckVerificationFields checks all verification fields of the object.

View file

@ -4,7 +4,7 @@ import (
"crypto/rand"
"testing"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neofs-sdk-go/crypto/test"
"github.com/stretchr/testify/require"
)
@ -17,9 +17,7 @@ func TestVerificationFields(t *testing.T) {
obj.SetPayload(payload)
obj.SetPayloadSize(uint64(len(payload)))
p, err := keys.NewPrivateKey()
require.NoError(t, err)
require.NoError(t, SetVerificationFields(p.PrivateKey, obj))
require.NoError(t, SetVerificationFields(test.RandomSigner(t), obj))
require.NoError(t, CheckVerificationFields(obj))

View file

@ -1,14 +1,12 @@
package oid
import (
"crypto/ecdsa"
"crypto/sha256"
"fmt"
"github.com/mr-tron/base58"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa"
)
// ID represents NeoFS object identifier in a container.
@ -117,7 +115,7 @@ func (id ID) String() string {
}
// CalculateIDSignature signs object id with provided key.
func (id ID) CalculateIDSignature(key ecdsa.PrivateKey) (neofscrypto.Signature, error) {
func (id ID) CalculateIDSignature(signer neofscrypto.Signer) (neofscrypto.Signature, error) {
data, err := id.Marshal()
if err != nil {
return neofscrypto.Signature{}, fmt.Errorf("marshal ID: %w", err)
@ -125,7 +123,7 @@ func (id ID) CalculateIDSignature(key ecdsa.PrivateKey) (neofscrypto.Signature,
var sig neofscrypto.Signature
return sig, sig.Calculate(neofsecdsa.Signer(key), data)
return sig, sig.Calculate(signer, data)
}
// Marshal marshals ID into a protobuf binary form.

View file

@ -338,6 +338,13 @@ func (o *Object) PreviousID() (v oid.ID, isSet bool) {
return
}
// ResetPreviousID resets identifier of the previous sibling object.
func (o *Object) ResetPreviousID() {
o.setSplitFields(func(split *object.SplitHeader) {
split.SetPrevious(nil)
})
}
// SetPreviousID sets identifier of the previous sibling object.
func (o *Object) SetPreviousID(v oid.ID) {
var v2 refs.ObjectID

View file

@ -12,7 +12,7 @@ import (
func TestInitCreation(t *testing.T) {
var o object.Object
cnr := cidtest.ID()
own := *usertest.ID()
own := *usertest.ID(t)
object.InitCreation(&o, object.RequiredFields{
Container: cnr,

View file

@ -1,40 +0,0 @@
package object
import (
"github.com/nspcc-dev/neofs-api-go/v2/object"
)
// RawObject represents v2-compatible NeoFS object that provides
// a convenient interface to fill in the fields of
// an object in isolation from its internal structure.
//
// Deprecated: use Object type instead.
type RawObject = Object
// NewRawFromV2 wraps v2 Object message to Object.
//
// Deprecated: (v1.0.0) use NewFromV2 function instead.
func NewRawFromV2(oV2 *object.Object) *Object {
return NewFromV2(oV2)
}
// NewRawFrom wraps Object instance to Object.
//
// Deprecated: (v1.0.0) function is no-op.
func NewRawFrom(obj *Object) *Object {
return obj
}
// NewRaw creates and initializes blank Object.
//
// Deprecated: (v1.0.0) use New instead.
func NewRaw() *Object {
return New()
}
// Object returns object instance.
//
// Deprecated: (v1.0.0) method is no-op, use arg directly.
func (o *Object) Object() *Object {
return o
}

View file

@ -1,318 +0,0 @@
package object
import (
"crypto/rand"
"crypto/sha256"
"testing"
"github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-sdk-go/checksum"
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test"
usertest "github.com/nspcc-dev/neofs-sdk-go/user/test"
"github.com/nspcc-dev/neofs-sdk-go/version"
"github.com/stretchr/testify/require"
)
func randID(t *testing.T) oid.ID {
var id oid.ID
id.SetSHA256(randSHA256Checksum(t))
return id
}
func randSHA256Checksum(t *testing.T) (cs [sha256.Size]byte) {
_, err := rand.Read(cs[:])
require.NoError(t, err)
return
}
func randTZChecksum(t *testing.T) (cs [64]byte) {
_, err := rand.Read(cs[:])
require.NoError(t, err)
return
}
func TestObject_SetID(t *testing.T) {
obj := New()
id := randID(t)
obj.SetID(id)
oID, set := obj.ID()
require.True(t, set)
require.Equal(t, id, oID)
}
func TestObject_SetPayload(t *testing.T) {
obj := New()
payload := make([]byte, 10)
_, _ = rand.Read(payload)
obj.SetPayload(payload)
require.Equal(t, payload, obj.Payload())
}
func TestObject_SetVersion(t *testing.T) {
obj := New()
var ver version.Version
ver.SetMajor(1)
ver.SetMinor(2)
obj.SetVersion(&ver)
require.Equal(t, ver, *obj.Version())
}
func TestObject_SetPayloadSize(t *testing.T) {
obj := New()
sz := uint64(133)
obj.SetPayloadSize(sz)
require.Equal(t, sz, obj.PayloadSize())
}
func TestObject_SetContainerID(t *testing.T) {
obj := New()
cid := cidtest.ID()
obj.SetContainerID(cid)
cID, set := obj.ContainerID()
require.True(t, set)
require.Equal(t, cid, cID)
}
func TestObject_SetOwnerID(t *testing.T) {
obj := New()
ownerID := usertest.ID()
obj.SetOwnerID(ownerID)
require.Equal(t, ownerID, obj.OwnerID())
}
func TestObject_SetCreationEpoch(t *testing.T) {
obj := New()
creat := uint64(228)
obj.SetCreationEpoch(creat)
require.Equal(t, creat, obj.CreationEpoch())
}
func TestObject_SetPayloadChecksum(t *testing.T) {
obj := New()
var cs checksum.Checksum
cs.SetSHA256(randSHA256Checksum(t))
obj.SetPayloadChecksum(cs)
cs2, set := obj.PayloadChecksum()
require.True(t, set)
require.Equal(t, cs, cs2)
}
func TestObject_SetPayloadHomomorphicHash(t *testing.T) {
obj := New()
var cs checksum.Checksum
cs.SetTillichZemor(randTZChecksum(t))
obj.SetPayloadHomomorphicHash(cs)
cs2, set := obj.PayloadHomomorphicHash()
require.True(t, set)
require.Equal(t, cs, cs2)
}
func TestObject_SetAttributes(t *testing.T) {
obj := New()
a1 := NewAttribute()
a1.SetKey("key1")
a1.SetValue("val1")
a2 := NewAttribute()
a2.SetKey("key2")
a2.SetValue("val2")
obj.SetAttributes(*a1, *a2)
require.Equal(t, []Attribute{*a1, *a2}, obj.Attributes())
}
func TestObject_SetPreviousID(t *testing.T) {
obj := New()
prev := randID(t)
obj.SetPreviousID(prev)
oID, set := obj.PreviousID()
require.True(t, set)
require.Equal(t, prev, oID)
}
func TestObject_SetChildren(t *testing.T) {
obj := New()
id1 := randID(t)
id2 := randID(t)
obj.SetChildren(id1, id2)
require.Equal(t, []oid.ID{id1, id2}, obj.Children())
}
func TestObject_SetSplitID(t *testing.T) {
obj := New()
require.Nil(t, obj.SplitID())
splitID := NewSplitID()
obj.SetSplitID(splitID)
require.Equal(t, obj.SplitID(), splitID)
}
func TestObject_SetParent(t *testing.T) {
obj := New()
require.Nil(t, obj.Parent())
par := New()
par.SetID(randID(t))
par.SetContainerID(cidtest.ID())
obj.SetParent(par)
require.Equal(t, par, obj.Parent())
}
func TestObject_ToV2(t *testing.T) {
objV2 := new(object.Object)
objV2.SetPayload([]byte{1, 2, 3})
obj := NewFromV2(objV2)
require.Equal(t, objV2, obj.ToV2())
}
func TestObject_SetSessionToken(t *testing.T) {
obj := New()
tok := sessiontest.ObjectSigned()
obj.SetSessionToken(tok)
require.Equal(t, tok, obj.SessionToken())
}
func TestObject_SetType(t *testing.T) {
obj := New()
typ := TypeStorageGroup
obj.SetType(typ)
require.Equal(t, typ, obj.Type())
}
func TestObject_CutPayload(t *testing.T) {
o1 := New()
p1 := []byte{12, 3}
o1.SetPayload(p1)
sz := uint64(13)
o1.SetPayloadSize(sz)
o2 := o1.CutPayload()
require.Equal(t, sz, o2.PayloadSize())
require.Empty(t, o2.Payload())
sz++
o1.SetPayloadSize(sz)
require.Equal(t, sz, o1.PayloadSize())
require.Equal(t, sz, o2.PayloadSize())
p2 := []byte{4, 5, 6}
o2.SetPayload(p2)
require.Equal(t, p2, o2.Payload())
require.Equal(t, p1, o1.Payload())
}
func TestObject_SetParentID(t *testing.T) {
obj := New()
id := randID(t)
obj.SetParentID(id)
oID, set := obj.ParentID()
require.True(t, set)
require.Equal(t, id, oID)
}
func TestObject_ResetRelations(t *testing.T) {
obj := New()
obj.SetPreviousID(randID(t))
obj.ResetRelations()
_, set := obj.PreviousID()
require.False(t, set)
}
func TestObject_HasParent(t *testing.T) {
obj := New()
obj.InitRelations()
require.True(t, obj.HasParent())
obj.ResetRelations()
require.False(t, obj.HasParent())
}
func TestObjectEncoding(t *testing.T) {
o := New()
o.SetID(randID(t))
o.SetContainerID(cidtest.ID())
t.Run("binary", func(t *testing.T) {
data, err := o.Marshal()
require.NoError(t, err)
o2 := New()
require.NoError(t, o2.Unmarshal(data))
require.Equal(t, o, o2)
})
t.Run("json", func(t *testing.T) {
data, err := o.MarshalJSON()
require.NoError(t, err)
o2 := New()
require.NoError(t, o2.UnmarshalJSON(data))
require.Equal(t, o, o2)
})
}

View file

@ -19,8 +19,9 @@ type Tokens struct {
}
type Relations interface {
// GetSplitInfo tries to get split info by root object id.
// If object isn't virtual it returns ErrNoSplitInfo.
// GetSplitInfo tries to get split info by some object id.
// This method must return split info on any object from split chain as well as on parent/linking object.
// If object doesn't have any split information returns ErrNoSplitInfo.
GetSplitInfo(ctx context.Context, cnrID cid.ID, rootID oid.ID, tokens Tokens) (*object.SplitInfo, error)
// ListChildrenByLinker returns list of children for link object.
@ -31,9 +32,6 @@ type Relations interface {
// If no previous object it returns ErrNoLeftSibling.
GetLeftSibling(ctx context.Context, cnrID cid.ID, objID oid.ID, tokens Tokens) (oid.ID, error)
// FindSiblingBySplitID returns all objects that relates to the provided split id.
FindSiblingBySplitID(ctx context.Context, cnrID cid.ID, splitID *object.SplitID, tokens Tokens) ([]oid.ID, error)
// FindSiblingByParentID returns all object that relates to the provided parent id.
FindSiblingByParentID(ctx context.Context, cnrID cid.ID, parentID oid.ID, tokens Tokens) ([]oid.ID, error)
}
@ -46,9 +44,15 @@ var (
ErrNoSplitInfo = errors.New("no split info")
)
// ListAllRelations return all related phy objects for provided root object ID.
// Result doesn't include root object ID itself.
// ListAllRelations return all related phy objects for provided root object ID in split-chain order.
// Result doesn't include root object ID itself. If linking object is found its id will be the last one.
func ListAllRelations(ctx context.Context, rels Relations, cnrID cid.ID, rootObjID oid.ID, tokens Tokens) ([]oid.ID, error) {
return ListRelations(ctx, rels, cnrID, rootObjID, tokens, true)
}
// ListRelations return all related phy objects for provided root object ID in split-chain order.
// Result doesn't include root object ID itself.
func ListRelations(ctx context.Context, rels Relations, cnrID cid.ID, rootObjID oid.ID, tokens Tokens, includeLinking bool) ([]oid.ID, error) {
splitInfo, err := rels.GetSplitInfo(ctx, cnrID, rootObjID, tokens)
if err != nil {
if errors.Is(err, ErrNoSplitInfo) {
@ -59,22 +63,40 @@ func ListAllRelations(ctx context.Context, rels Relations, cnrID cid.ID, rootObj
// collect split chain by the descending ease of operations (ease is evaluated heuristically).
// If any approach fails, we don't try the next since we assume that it will fail too.
if _, ok := splitInfo.Link(); !ok {
// the list is expected to contain last part and (probably) split info
list, err := rels.FindSiblingByParentID(ctx, cnrID, rootObjID, tokens)
if err != nil {
return nil, fmt.Errorf("failed to find object children: %w", err)
}
for _, id := range list {
split, err := rels.GetSplitInfo(ctx, cnrID, id, tokens)
if err != nil {
if errors.Is(err, ErrNoSplitInfo) {
continue
}
return nil, fmt.Errorf("failed to get split info: %w", err)
}
if link, ok := split.Link(); ok {
splitInfo.SetLink(link)
}
if last, ok := split.LastPart(); ok {
splitInfo.SetLastPart(last)
}
}
}
if idLinking, ok := splitInfo.Link(); ok {
children, err := rels.ListChildrenByLinker(ctx, cnrID, idLinking, tokens)
if err != nil {
return nil, fmt.Errorf("failed to get linking object's header: %w", err)
}
// include linking object
return append(children, idLinking), nil
if includeLinking {
children = append(children, idLinking)
}
if idSplit := splitInfo.SplitID(); idSplit != nil {
members, err := rels.FindSiblingBySplitID(ctx, cnrID, idSplit, tokens)
if err != nil {
return nil, fmt.Errorf("failed to search objects by split ID: %w", err)
}
return members, nil
return children, nil
}
idMember, ok := splitInfo.LastPart()
@ -85,9 +107,6 @@ func ListAllRelations(ctx context.Context, rels Relations, cnrID cid.ID, rootObj
chain := []oid.ID{idMember}
chainSet := map[oid.ID]struct{}{idMember: {}}
// prmHead.SetRawFlag(false)
// split members are almost definitely singular, but don't get hung up on it
for {
idMember, err = rels.GetLeftSibling(ctx, cnrID, idMember, tokens)
if err != nil {
@ -101,20 +120,9 @@ func ListAllRelations(ctx context.Context, rels Relations, cnrID cid.ID, rootObj
return nil, fmt.Errorf("duplicated member in the split chain %s", idMember)
}
chain = append(chain, idMember)
chain = append([]oid.ID{idMember}, chain...)
chainSet[idMember] = struct{}{}
}
list, err := rels.FindSiblingByParentID(ctx, cnrID, rootObjID, tokens)
if err != nil {
return nil, fmt.Errorf("failed to find object children: %w", err)
}
for i := range list {
if _, ok = chainSet[list[i]]; !ok {
chain = append(chain, list[i])
}
}
return chain, nil
}

View file

@ -1,6 +1,8 @@
package object
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"strconv"
@ -9,6 +11,7 @@ import (
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/user"
"github.com/nspcc-dev/neofs-sdk-go/version"
"github.com/nspcc-dev/tzhash/tz"
)
// SearchMatchType indicates match operation on specified header.
@ -54,7 +57,7 @@ func SearchMatchFromV2(t v2object.MatchType) (m SearchMatchType) {
return m
}
// String returns string representation of SearchMatchType.
// EncodeToString returns string representation of SearchMatchType.
//
// String mapping:
// - MatchStringEqual: STRING_EQUAL;
@ -62,15 +65,24 @@ func SearchMatchFromV2(t v2object.MatchType) (m SearchMatchType) {
// - MatchNotPresent: NOT_PRESENT;
// - MatchCommonPrefix: COMMON_PREFIX;
// - MatchUnknown, default: MATCH_TYPE_UNSPECIFIED.
func (m SearchMatchType) String() string {
func (m SearchMatchType) EncodeToString() string {
return m.ToV2().String()
}
// FromString parses SearchMatchType from a string representation.
// It is a reverse action to String().
// String implements fmt.Stringer.
//
// String is designed to be human-readable, and its format MAY differ between
// SDK versions. String MAY return same result as EncodeToString. String MUST NOT
// be used to encode ID into NeoFS protocol string.
func (m SearchMatchType) String() string {
return m.EncodeToString()
}
// DecodeString parses SearchMatchType from a string representation.
// It is a reverse action to EncodeToString().
//
// Returns true if s was parsed successfully.
func (m *SearchMatchType) FromString(s string) bool {
func (m *SearchMatchType) DecodeString(s string) bool {
var g v2object.MatchType
ok := g.FromString(s)
@ -279,7 +291,7 @@ func (f *SearchFilters) AddSplitIDFilter(m SearchMatchType, id *SplitID) {
// AddTypeFilter adds filter by object type.
func (f *SearchFilters) AddTypeFilter(m SearchMatchType, typ Type) {
f.addReservedFilter(m, fKeyType, staticStringer(typ.String()))
f.addReservedFilter(m, fKeyType, staticStringer(typ.EncodeToString()))
}
// MarshalJSON encodes SearchFilters to protobuf JSON format.
@ -299,3 +311,13 @@ func (f *SearchFilters) UnmarshalJSON(data []byte) error {
return nil
}
// AddPayloadHashFilter adds filter by payload hash.
func (f *SearchFilters) AddPayloadHashFilter(m SearchMatchType, sum [sha256.Size]byte) {
f.addReservedFilter(m, fKeyPayloadHash, staticStringer(hex.EncodeToString(sum[:])))
}
// AddHomomorphicHashFilter adds filter by homomorphic hash.
func (f *SearchFilters) AddHomomorphicHashFilter(m SearchMatchType, sum [tz.Size]byte) {
f.addReservedFilter(m, fKeyHomomorphicHash, staticStringer(hex.EncodeToString(sum[:])))
}

View file

@ -2,12 +2,16 @@ package object_test
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"math/rand"
"testing"
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-sdk-go/checksum"
"github.com/nspcc-dev/neofs-sdk-go/object"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/tzhash/tz"
"github.com/stretchr/testify/require"
)
@ -172,7 +176,7 @@ func TestSearchFilters_AddTypeFilter(t *testing.T) {
require.Len(t, fsV2, 1)
require.Equal(t, v2object.FilterHeaderObjectType, fsV2[0].GetKey())
require.Equal(t, typ.String(), fsV2[0].GetValue())
require.Equal(t, typ.EncodeToString(), fsV2[0].GetValue())
require.Equal(t, v2object.MatchStringEqual, fsV2[0].GetMatchType())
})
}
@ -207,3 +211,77 @@ func TestSearchMatchType_String(t *testing.T) {
{val: toPtr(object.MatchUnknown), str: "MATCH_TYPE_UNSPECIFIED"},
})
}
func testChecksumSha256() [sha256.Size]byte {
cs := [sha256.Size]byte{}
rand.Read(cs[:])
return cs
}
func testChecksumTZ() [tz.Size]byte {
cs := [tz.Size]byte{}
rand.Read(cs[:])
return cs
}
func TestSearchFilters_AddPayloadHashFilter(t *testing.T) {
cs := testChecksumSha256()
fs := new(object.SearchFilters)
fs.AddPayloadHashFilter(object.MatchStringEqual, cs)
t.Run("v2", func(t *testing.T) {
fsV2 := fs.ToV2()
require.Len(t, fsV2, 1)
require.Equal(t, v2object.FilterHeaderPayloadHash, fsV2[0].GetKey())
require.Equal(t, hex.EncodeToString(cs[:]), fsV2[0].GetValue())
require.Equal(t, v2object.MatchStringEqual, fsV2[0].GetMatchType())
})
}
func ExampleSearchFilters_AddPayloadHashFilter() {
hash, _ := hex.DecodeString("66842cfea090b1d906b52400fae49d86df078c0670f2bdd059ba289ebe24a498")
var v [sha256.Size]byte
copy(v[:], hash[:sha256.Size])
var cs checksum.Checksum
cs.SetSHA256(v)
fmt.Println(hex.EncodeToString(cs.Value()))
// Output: 66842cfea090b1d906b52400fae49d86df078c0670f2bdd059ba289ebe24a498
}
func TestSearchFilters_AddHomomorphicHashFilter(t *testing.T) {
cs := testChecksumTZ()
fs := new(object.SearchFilters)
fs.AddHomomorphicHashFilter(object.MatchStringEqual, cs)
t.Run("v2", func(t *testing.T) {
fsV2 := fs.ToV2()
require.Len(t, fsV2, 1)
require.Equal(t, v2object.FilterHeaderHomomorphicHash, fsV2[0].GetKey())
require.Equal(t, hex.EncodeToString(cs[:]), fsV2[0].GetValue())
require.Equal(t, v2object.MatchStringEqual, fsV2[0].GetMatchType())
})
}
func ExampleSearchFilters_AddHomomorphicHashFilter() {
hash, _ := hex.DecodeString("7e302ebb3937e810feb501965580c746048db99cebd095c3ce27022407408bf904dde8d9aa8085d2cf7202345341cc947fa9d722c6b6699760d307f653815d0c")
var v [tz.Size]byte
copy(v[:], hash[:tz.Size])
var cs checksum.Checksum
cs.SetTillichZemor(v)
fmt.Println(hex.EncodeToString(cs.Value()))
// Output: 7e302ebb3937e810feb501965580c746048db99cebd095c3ce27022407408bf904dde8d9aa8085d2cf7202345341cc947fa9d722c6b6699760d307f653815d0c
}

4
object/slicer/doc.go Normal file
View file

@ -0,0 +1,4 @@
/*
Package slicer provides raw data slicing into NeoFS objects.
*/
package slicer

27
object/slicer/options.go Normal file
View file

@ -0,0 +1,27 @@
package slicer
// Options groups Slicer options.
type Options struct {
objectPayloadLimit uint64
currentNeoFSEpoch uint64
withHomoChecksum bool
}
// SetObjectPayloadLimit specifies data size limit for produced physically
// stored objects.
func (x *Options) SetObjectPayloadLimit(l uint64) {
x.objectPayloadLimit = l
}
// SetCurrentNeoFSEpoch sets current NeoFS epoch.
func (x *Options) SetCurrentNeoFSEpoch(e uint64) {
x.currentNeoFSEpoch = e
}
// CalculateHomomorphicChecksum makes Slicer to calculate and set homomorphic
// checksum of the processed objects.
func (x *Options) CalculateHomomorphicChecksum() {
x.withHomoChecksum = true
}

648
object/slicer/slicer.go Normal file
View file

@ -0,0 +1,648 @@
package slicer
import (
"bytes"
"crypto/sha256"
"errors"
"fmt"
"hash"
"io"
"github.com/nspcc-dev/neofs-sdk-go/checksum"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
"github.com/nspcc-dev/neofs-sdk-go/object"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/session"
"github.com/nspcc-dev/neofs-sdk-go/user"
"github.com/nspcc-dev/neofs-sdk-go/version"
"github.com/nspcc-dev/tzhash/tz"
)
var (
// ErrInvalidAttributeAmount indicates wrong number of arguments. Amount of arguments MUST be even number.
ErrInvalidAttributeAmount = errors.New("attributes must be even number of strings")
)
// ObjectWriter represents a virtual object recorder.
type ObjectWriter interface {
// InitDataStream initializes and returns a stream of writable data associated
// with the object according to its header. Provided header includes at least
// container, owner and object ID fields.
InitDataStream(header object.Object) (dataStream io.Writer, err error)
}
// Slicer converts input raw data streams into NeoFS objects. Working Slicer
// must be constructed via New.
type Slicer struct {
signer neofscrypto.Signer
cnr cid.ID
owner user.ID
w ObjectWriter
opts Options
sessionToken *session.Object
}
// New constructs Slicer which writes sliced ready-to-go objects owned by
// particular user into the specified container using provided ObjectWriter.
// All objects are signed using provided neofscrypto.Signer.
//
// If ObjectWriter returns data streams which provide io.Closer, they are closed
// in Slicer.Slice after the payload of any object has been written. In this
// case, Slicer.Slice fails immediately on Close error.
//
// Options parameter allows you to provide optional parameters which tune
// the default Slicer behavior. They are detailed below.
//
// If payload size limit is specified via Options.SetObjectPayloadLimit,
// outgoing objects has payload not bigger than the limit. NeoFS stores the
// corresponding value in the network configuration. Ignore this option if you
// don't (want to) have access to it. By default, single object is limited by
// 1MB. Slicer uses this value to enforce the maximum object payload size limit
// described in the NeoFS Specification. If the total amount of data exceeds the
// specified limit, Slicer applies the slicing algorithm described within the
// same specification. The outcome will be a group of "small" objects containing
// a chunk of data, as well as an auxiliary linking object. All derived objects
// are written to the parameterized ObjectWriter. If the amount of data is
// within the limit, one object is produced. Note that Slicer can write multiple
// objects, but returns the root object ID only.
//
// If current NeoFS epoch is specified via Options.SetCurrentNeoFSEpoch, it is
// written to the metadata of all resulting objects as a creation epoch.
//
// See also NewSession.
func New(signer neofscrypto.Signer, cnr cid.ID, owner user.ID, w ObjectWriter, opts Options) *Slicer {
return &Slicer{
signer: signer,
cnr: cnr,
owner: owner,
w: w,
opts: opts,
}
}
// NewSession creates Slicer which generates objects within provided session.
// NewSession work similar to New with the detail that the session issuer owns
// the produced objects. Specified session token is written to the metadata of
// all resulting objects. In this case, the object is considered to be created
// by a proxy on behalf of the session issuer.
func NewSession(signer neofscrypto.Signer, cnr cid.ID, token session.Object, w ObjectWriter, opts Options) *Slicer {
return &Slicer{
signer: signer,
cnr: cnr,
owner: token.Issuer(),
w: w,
opts: opts,
sessionToken: &token,
}
}
// fillCommonMetadata writes to the object metadata common to all objects of the
// same stream.
func (x *Slicer) fillCommonMetadata(obj *object.Object) {
currentVersion := version.Current()
obj.SetVersion(&currentVersion)
obj.SetContainerID(x.cnr)
obj.SetCreationEpoch(x.opts.currentNeoFSEpoch)
obj.SetType(object.TypeRegular)
obj.SetOwnerID(&x.owner)
obj.SetSessionToken(x.sessionToken)
}
const defaultPayloadSizeLimit = 1 << 20
// childPayloadSizeLimit returns configured size limit of the child object's
// payload which defaults to 1MB.
func (x *Slicer) childPayloadSizeLimit() uint64 {
if x.opts.objectPayloadLimit > 0 {
return x.opts.objectPayloadLimit
}
return defaultPayloadSizeLimit
}
// Slice creates new NeoFS object from the input data stream, associates the
// object with the configured container and writes the object via underlying
// ObjectWriter. After a successful write, Slice returns an oid.ID which is a
// unique reference to the object in the container. Slice sets all required
// calculated fields like payload length, checksum, etc.
//
// Slice allows you to specify string key-value pairs to be written to the
// resulting object's metadata as object attributes. Corresponding argument MUST
// NOT be empty or have odd length. Keys SHOULD NOT start with system-reserved
// '__NEOFS__' prefix.
//
// See New for details.
func (x *Slicer) Slice(data io.Reader, attributes ...string) (oid.ID, error) {
var rootID oid.ID
if len(attributes)%2 != 0 {
return rootID, ErrInvalidAttributeAmount
}
if x.opts.objectPayloadLimit == 0 {
x.opts.objectPayloadLimit = 1 << 20
}
var rootHeader object.Object
var offset uint64
var isSplit bool
var childMeta dynamicObjectMetadata
var writtenChildren []oid.ID
var childHeader object.Object
rootMeta := newDynamicObjectMetadata(x.opts.withHomoChecksum)
bChunk := make([]byte, x.opts.objectPayloadLimit+1)
x.fillCommonMetadata(&rootHeader)
for {
n, err := data.Read(bChunk[offset:])
if err == nil {
if last := offset + uint64(n); last <= x.opts.objectPayloadLimit {
rootMeta.accumulateNextPayloadChunk(bChunk[offset:last])
if isSplit {
childMeta.accumulateNextPayloadChunk(bChunk[offset:last])
}
offset = last
// data is not over, and we expect more bytes to form next object
continue
}
} else {
if !errors.Is(err, io.EOF) {
return rootID, fmt.Errorf("read payload chunk: %w", err)
}
// there will be no more data
toSend := offset + uint64(n)
if toSend <= x.opts.objectPayloadLimit {
// we can finalize the root object and send last part
if len(attributes) > 0 {
attrs := make([]object.Attribute, len(attributes)/2)
for i := 0; i < len(attrs); i++ {
attrs[i].SetKey(attributes[2*i])
attrs[i].SetValue(attributes[2*i+1])
}
rootHeader.SetAttributes(attrs...)
}
rootID, err = flushObjectMetadata(x.signer, rootMeta, &rootHeader)
if err != nil {
return rootID, fmt.Errorf("form root object: %w", err)
}
if isSplit {
// when splitting, root object's header is written into its last child
childHeader.SetParent(&rootHeader)
childHeader.SetPreviousID(writtenChildren[len(writtenChildren)-1])
childID, err := writeInMemObject(x.signer, x.w, childHeader, bChunk[:toSend], childMeta)
if err != nil {
return rootID, fmt.Errorf("write child object: %w", err)
}
writtenChildren = append(writtenChildren, childID)
} else {
// root object is single (full < limit), so send it directly
rootID, err = writeInMemObject(x.signer, x.w, rootHeader, bChunk[:toSend], rootMeta)
if err != nil {
return rootID, fmt.Errorf("write single root object: %w", err)
}
return rootID, nil
}
break
}
// otherwise, form penultimate object, then do one more iteration for
// simplicity: according to io.Reader, we'll get io.EOF again, but the overflow
// will no longer occur, so we'll finish the loop
}
// according to buffer size, here we can overflow the object payload limit, e.g.
// 1. full=11B,limit=10B,read=11B (no objects created yet)
// 2. full=21B,limit=10B,read=11B (one object has been already sent with size=10B)
toSend := offset + uint64(n)
overflow := toSend > x.opts.objectPayloadLimit
if overflow {
toSend = x.opts.objectPayloadLimit
}
// we could read some data even in case of io.EOF, so don't forget pick up the tail
if n > 0 {
rootMeta.accumulateNextPayloadChunk(bChunk[offset:toSend])
if isSplit {
childMeta.accumulateNextPayloadChunk(bChunk[offset:toSend])
}
}
if overflow {
isSplitCp := isSplit // we modify it in next condition below but need after it
if !isSplit {
// we send only child object below, but we can get here at the beginning (see
// option 1 described above), so we need to pre-init child resources
isSplit = true
x.fillCommonMetadata(&childHeader)
childHeader.SetSplitID(object.NewSplitID())
childMeta = rootMeta
// we do shallow copy of rootMeta because below we take this into account and do
// not corrupt it
} else {
childHeader.SetPreviousID(writtenChildren[len(writtenChildren)-1])
}
childID, err := writeInMemObject(x.signer, x.w, childHeader, bChunk[:toSend], childMeta)
if err != nil {
return rootID, fmt.Errorf("write child object: %w", err)
}
writtenChildren = append(writtenChildren, childID)
// shift overflow bytes to the beginning
if !isSplitCp {
childMeta = newDynamicObjectMetadata(x.opts.withHomoChecksum) // to avoid rootMeta corruption
}
childMeta.reset()
childMeta.accumulateNextPayloadChunk(bChunk[toSend:])
rootMeta.accumulateNextPayloadChunk(bChunk[toSend:])
offset = uint64(copy(bChunk, bChunk[toSend:]))
}
}
// linking object
childMeta.reset()
childHeader.ResetPreviousID()
childHeader.SetChildren(writtenChildren...)
_, err := writeInMemObject(x.signer, x.w, childHeader, nil, childMeta)
if err != nil {
return rootID, fmt.Errorf("write linking object: %w", err)
}
return rootID, nil
}
// InitPayloadStream works similar to Slice but provides PayloadWriter allowing
// the caller to write data himself.
func (x *Slicer) InitPayloadStream(attributes ...string) (*PayloadWriter, error) {
res := &PayloadWriter{
stream: x.w,
signer: x.signer,
container: x.cnr,
owner: x.owner,
currentEpoch: x.opts.currentNeoFSEpoch,
sessionToken: x.sessionToken,
attributes: attributes,
rootMeta: newDynamicObjectMetadata(x.opts.withHomoChecksum),
childMeta: newDynamicObjectMetadata(x.opts.withHomoChecksum),
}
res.buf.Grow(int(x.childPayloadSizeLimit()))
res.rootMeta.reset()
res.currentWriter = newLimitedWriter(io.MultiWriter(&res.buf, &res.rootMeta), x.childPayloadSizeLimit())
return res, nil
}
// PayloadWriter is a single-object payload stream provided by Slicer.
type PayloadWriter struct {
stream ObjectWriter
rootID oid.ID
signer neofscrypto.Signer
container cid.ID
owner user.ID
currentEpoch uint64
sessionToken *session.Object
attributes []string
buf bytes.Buffer
rootMeta dynamicObjectMetadata
childMeta dynamicObjectMetadata
currentWriter limitedWriter
withSplit bool
writtenChildren []oid.ID
}
// Write writes next chunk of the object data. Concatenation of all chunks forms
// the payload of the final object. When the data is over, the PayloadWriter
// should be closed.
func (x *PayloadWriter) Write(chunk []byte) (int, error) {
if len(chunk) == 0 {
// not explicitly prohibited in the io.Writer documentation
return 0, nil
}
n, err := x.currentWriter.Write(chunk)
if err == nil || !errors.Is(err, errOverflow) {
return n, err
}
if !x.withSplit {
err = x.writeIntermediateChild(x.rootMeta)
if err != nil {
return n, fmt.Errorf("write 1st child: %w", err)
}
x.currentWriter.reset(io.MultiWriter(&x.buf, &x.rootMeta, &x.childMeta))
x.withSplit = true
} else {
err = x.writeIntermediateChild(x.childMeta)
if err != nil {
return n, fmt.Errorf("write next child: %w", err)
}
x.currentWriter.resetProgress()
}
x.buf.Reset()
x.childMeta.reset()
n2, err := x.Write(chunk[n:]) // here n > 0 so infinite recursion shouldn't occur
return n + n2, err
}
// Close finalizes object with written payload data, saves the object and closes
// the stream. Reference to the stored object can be obtained by ID method.
func (x *PayloadWriter) Close() error {
if x.withSplit {
return x.writeLastChild(x.childMeta, x.setID)
}
return x.writeLastChild(x.rootMeta, x.setID)
}
func (x *PayloadWriter) setID(id oid.ID) {
x.rootID = id
}
// ID returns unique identifier of the stored object representing its reference
// in the configured container.
//
// ID MUST NOT be called before successful Close (undefined behavior otherwise).
func (x *PayloadWriter) ID() oid.ID {
return x.rootID
}
// writeIntermediateChild writes intermediate split-chain element with specified
// dynamicObjectMetadata to the configured ObjectWriter.
func (x *PayloadWriter) writeIntermediateChild(meta dynamicObjectMetadata) error {
return x._writeChild(meta, false, nil)
}
// writeIntermediateChild writes last split-chain element with specified
// dynamicObjectMetadata to the configured ObjectWriter. If rootIDHandler is
// specified, ID of the resulting root object is passed into it.
func (x *PayloadWriter) writeLastChild(meta dynamicObjectMetadata, rootIDHandler func(id oid.ID)) error {
return x._writeChild(meta, true, rootIDHandler)
}
func (x *PayloadWriter) _writeChild(meta dynamicObjectMetadata, last bool, rootIDHandler func(id oid.ID)) error {
currentVersion := version.Current()
fCommon := func(obj *object.Object) {
obj.SetVersion(&currentVersion)
obj.SetContainerID(x.container)
obj.SetCreationEpoch(x.currentEpoch)
obj.SetType(object.TypeRegular)
obj.SetOwnerID(&x.owner)
obj.SetSessionToken(x.sessionToken)
}
var obj object.Object
fCommon(&obj)
if len(x.writtenChildren) > 0 {
obj.SetPreviousID(x.writtenChildren[len(x.writtenChildren)-1])
}
if last {
var rootObj *object.Object
if x.withSplit {
rootObj = new(object.Object)
} else {
rootObj = &obj
}
fCommon(rootObj)
if len(x.attributes) > 0 {
attrs := make([]object.Attribute, len(x.attributes)/2)
for i := 0; i < len(attrs); i++ {
attrs[i].SetKey(x.attributes[2*i])
attrs[i].SetValue(x.attributes[2*i+1])
}
rootObj.SetAttributes(attrs...)
}
rootID, err := flushObjectMetadata(x.signer, x.rootMeta, rootObj)
if err != nil {
return fmt.Errorf("form root object: %w", err)
}
if rootIDHandler != nil {
rootIDHandler(rootID)
}
if x.withSplit {
obj.SetParentID(rootID)
obj.SetParent(rootObj)
}
}
id, err := writeInMemObject(x.signer, x.stream, obj, x.buf.Bytes(), meta)
if err != nil {
return fmt.Errorf("write formed object: %w", err)
}
x.writtenChildren = append(x.writtenChildren, id)
if x.withSplit && last {
obj.ResetPreviousID()
obj.SetChildren(x.writtenChildren...)
_, err = writeInMemObject(x.signer, x.stream, obj, nil, meta)
if err != nil {
return fmt.Errorf("write linking object: %w", err)
}
}
return nil
}
func flushObjectMetadata(signer neofscrypto.Signer, meta dynamicObjectMetadata, header *object.Object) (oid.ID, error) {
var cs checksum.Checksum
var csBytes [sha256.Size]byte
copy(csBytes[:], meta.checksum.Sum(nil))
cs.SetSHA256(csBytes)
header.SetPayloadChecksum(cs)
if meta.homomorphicChecksum != nil {
var csHomoBytes [tz.Size]byte
copy(csHomoBytes[:], meta.homomorphicChecksum.Sum(nil))
cs.SetTillichZemor(csHomoBytes)
header.SetPayloadHomomorphicHash(cs)
}
header.SetPayloadSize(meta.length)
id, err := object.CalculateID(header)
if err != nil {
return id, fmt.Errorf("calculate ID: %w", err)
}
header.SetID(id)
bID, err := id.Marshal()
if err != nil {
return id, fmt.Errorf("marshal object ID: %w", err)
}
var sig neofscrypto.Signature
err = sig.Calculate(signer, bID)
if err != nil {
return id, fmt.Errorf("sign object ID: %w", err)
}
header.SetSignature(&sig)
return id, nil
}
func writeInMemObject(signer neofscrypto.Signer, w ObjectWriter, header object.Object, payload []byte, meta dynamicObjectMetadata) (oid.ID, error) {
id, err := flushObjectMetadata(signer, meta, &header)
if err != nil {
return id, err
}
stream, err := w.InitDataStream(header)
if err != nil {
return id, fmt.Errorf("init data stream for next object: %w", err)
}
_, err = stream.Write(payload)
if err != nil {
return id, fmt.Errorf("write object payload: %w", err)
}
if c, ok := stream.(io.Closer); ok {
err = c.Close()
if err != nil {
return id, fmt.Errorf("finish object stream: %w", err)
}
}
return id, nil
}
// dynamicObjectMetadata groups accumulated object metadata which depends on
// payload.
type dynamicObjectMetadata struct {
length uint64
checksum hash.Hash
homomorphicChecksum hash.Hash
}
func newDynamicObjectMetadata(withHomoChecksum bool) dynamicObjectMetadata {
m := dynamicObjectMetadata{
checksum: sha256.New(),
}
if withHomoChecksum {
m.homomorphicChecksum = tz.New()
}
return m
}
func (x *dynamicObjectMetadata) Write(chunk []byte) (int, error) {
x.accumulateNextPayloadChunk(chunk)
return len(chunk), nil
}
// accumulateNextPayloadChunk handles the next payload chunk and updates the
// accumulated metadata.
func (x *dynamicObjectMetadata) accumulateNextPayloadChunk(chunk []byte) {
x.length += uint64(len(chunk))
x.checksum.Write(chunk)
if x.homomorphicChecksum != nil {
x.homomorphicChecksum.Write(chunk)
}
}
// reset resets all accumulated metadata.
func (x *dynamicObjectMetadata) reset() {
x.length = 0
x.checksum.Reset()
if x.homomorphicChecksum != nil {
x.homomorphicChecksum.Reset()
}
}
var errOverflow = errors.New("overflow")
// limitedWriter provides io.Writer limiting data volume.
type limitedWriter struct {
base io.Writer
limit, written uint64
}
// newLimitedWriter initializes limiterWriter which writes data to the base
// writer before the specified limit.
func newLimitedWriter(base io.Writer, limit uint64) limitedWriter {
return limitedWriter{
base: base,
limit: limit,
}
}
// reset resets progress to zero and sets the base target for writing subsequent
// data.
func (x *limitedWriter) reset(base io.Writer) {
x.base = base
x.resetProgress()
}
// resetProgress resets progress to zero.
func (x *limitedWriter) resetProgress() {
x.written = 0
}
// Write writes next chunk of the data to the base writer. If chunk along with
// already written data overflows configured limit, Write returns errOverflow.
func (x *limitedWriter) Write(p []byte) (n int, err error) {
overflow := uint64(len(p)) > x.limit-x.written
if overflow {
n, err = x.base.Write(p[:x.limit-x.written])
} else {
n, err = x.base.Write(p)
}
x.written += uint64(n)
if overflow && err == nil {
return n, errOverflow
}
return n, err
}

Some files were not shown because too many files have changed in this diff Show more