11package command
22
33import (
4+ "bytes"
45 "fmt"
6+ "io"
57 "strings"
8+
9+ jsg "github.com/alanshaw/dag-json-gen"
10+ cbg "github.com/whyrusleeping/cbor-gen"
611)
712
813const separator = "/"
@@ -13,11 +18,33 @@ const separator = "/"
1318// A [Command] is composed of a leading slash which is optionally followed by
1419// one or more slash-separated Segments of lowercase characters.
1520//
21+ // The underlying field is unexported so a Command can only be obtained from a
22+ // validating constructor ([New], [Parse], [MustParse] or [Top]). This makes
23+ // invalid Commands unrepresentable: a value of this type is always either the
24+ // undefined zero value or a well-formed command.
25+ //
26+ // Note: this is a struct rather than `type Command string` for two reasons —
27+ // it prevents arbitrary strings being converted into a Command, and cbor-gen
28+ // only recognises MarshalCBOR/UnmarshalCBOR on non-string (struct) types.
29+ //
1630// [Command]: https://github.com/ucan-wg/spec#command
17- type Command string
31+ type Command struct {
32+ str string
33+ }
34+
35+ // Undef is the zero value of Command, representing an undefined command. Using
36+ // Command{} directly is also acceptable.
37+ var Undef = Command {}
1838
19- // New creates a validated command from the provided list of segment strings. An
20- // error is returned if an invalid Command would be formed
39+ // Defined reports whether the Command holds a value (i.e. is not the undefined
40+ // zero value).
41+ func (c Command ) Defined () bool {
42+ return c .str != ""
43+ }
44+
45+ // New creates a command from the provided segments. Segments are assumed to be
46+ // well-formed; New does not validate them. To validate untrusted input, use
47+ // [Parse].
2148func New (segments ... string ) Command {
2249 return Top ().Join (segments ... )
2350}
@@ -28,20 +55,30 @@ func New(segments ...string) Command {
2855// [segment structure]: https://github.com/ucan-wg/spec#segment-structure
2956func Parse (s string ) (Command , error ) {
3057 if ! strings .HasPrefix (s , "/" ) {
31- return "" , ErrRequiresLeadingSlash
58+ return Undef , ErrRequiresLeadingSlash
3259 }
3360
3461 if len (s ) > 1 && strings .HasSuffix (s , "/" ) {
35- return "" , ErrDisallowsTrailingSlash
62+ return Undef , ErrDisallowsTrailingSlash
3663 }
3764
3865 if s != strings .ToLower (s ) {
39- return "" , ErrRequiresLowercase
66+ return Undef , ErrRequiresLowercase
4067 }
4168
42- // The leading slash will result in the first element from strings. Split
69+ // The leading slash will result in the first element from strings.Split
4370 // being an empty string which is removed as strings.Join will ignore it.
44- return Command (s ), nil
71+ return Command {str : s }, nil
72+ }
73+
74+ // MustParse is like [Parse] but panics if s is not a valid Command. It is
75+ // intended for package-level command definitions from constant strings.
76+ func MustParse (s string ) Command {
77+ cmd , err := Parse (s )
78+ if err != nil {
79+ panic (fmt .Sprintf ("command: MustParse(%q): %v" , s , err ))
80+ }
81+ return cmd
4582}
4683
4784// Top is the most powerful capability.
@@ -52,7 +89,7 @@ func Parse(s string) (Command, error) {
5289//
5390// [Top]: https://github.com/ucan-wg/spec#-aka-top
5491func Top () Command {
55- return Command ( separator )
92+ return Command { str : separator }
5693}
5794
5895// Join appends segments to the end of this command using the required
@@ -65,8 +102,8 @@ func (c Command) Join(segments ...string) Command {
65102 if size == 0 {
66103 return c
67104 }
68- buf := make ([]byte , 0 , len (c )+ size + len (segments ))
69- buf = append (buf , []byte (c )... )
105+ buf := make ([]byte , 0 , len (c . str )+ size + len (segments ))
106+ buf = append (buf , []byte (c . str )... )
70107 for _ , s := range segments {
71108 if s != "" {
72109 if len (buf ) > 1 {
@@ -75,16 +112,16 @@ func (c Command) Join(segments ...string) Command {
75112 buf = append (buf , []byte (s )... )
76113 }
77114 }
78- return Command (buf )
115+ return Command { str : string (buf )}
79116}
80117
81118// Segments returns the ordered segments that comprise the Command as a slice
82119// of strings.
83120func (c Command ) Segments () []string {
84- if c == separator {
121+ if c . str == separator {
85122 return nil
86123 }
87- return strings .Split (string ( c ) , separator )[1 :]
124+ return strings .Split (c . str , separator )[1 :]
88125}
89126
90127// Proves returns true if the command is identical or a parent of the given
@@ -96,16 +133,93 @@ func (c Command) Segments() []string {
96133// https://github.com/ucan-wg/spec/blob/main/README.md#segment-structure
97134func (c Command ) Proves (other Command ) bool {
98135 // fast-path, equivalent to the code below (verified with fuzzing)
99- if ! strings .HasPrefix (string ( other ), string ( c ) ) {
136+ if ! strings .HasPrefix (other . str , c . str ) {
100137 return false
101138 }
102- return c == separator || len (c ) == len (other ) || other [len (c )] == separator [0 ]
139+ return c . str == separator || len (c . str ) == len (other . str ) || other . str [len (c . str )] == separator [0 ]
103140}
104141
105142// String returns the composed representation the command. This is also the
106143// required wire representation (before IPLD encoding occurs).
107144func (c Command ) String () string {
108- return string (c )
145+ return c .str
146+ }
147+
148+ func (c Command ) MarshalJSON () ([]byte , error ) {
149+ var buf bytes.Buffer
150+ if err := c .MarshalDagJSON (& buf ); err != nil {
151+ return nil , err
152+ }
153+ return buf .Bytes (), nil
154+ }
155+
156+ func (c * Command ) UnmarshalJSON (b []byte ) error {
157+ return c .UnmarshalDagJSON (bytes .NewReader (b ))
158+ }
159+
160+ func (c Command ) MarshalCBOR (w io.Writer ) error {
161+ if c .str == "" {
162+ _ , err := w .Write (cbg .CborNull )
163+ return err
164+ }
165+ cw := cbg .NewCborWriter (w )
166+ if err := cw .WriteMajorTypeHeader (cbg .MajTextString , uint64 (len (c .str ))); err != nil {
167+ return err
168+ }
169+ _ , err := cw .WriteString (c .str )
170+ return err
171+ }
172+
173+ func (c * Command ) UnmarshalCBOR (r io.Reader ) error {
174+ cr := cbg .NewCborReader (r )
175+ b , err := cr .ReadByte ()
176+ if err != nil {
177+ return err
178+ }
179+ if b != cbg .CborNull [0 ] {
180+ if err := cr .UnreadByte (); err != nil {
181+ return err
182+ }
183+ str , err := cbg .ReadStringWithMax (cr , 2048 )
184+ if err != nil {
185+ return err
186+ }
187+ parsed , err := Parse (str )
188+ if err != nil {
189+ return err
190+ }
191+ * c = parsed
192+ }
193+ return nil
194+ }
195+
196+ func (c Command ) MarshalDagJSON (w io.Writer ) error {
197+ jw := jsg .NewDagJsonWriter (w )
198+ if c .str == "" {
199+ return jw .WriteNull ()
200+ }
201+ return jw .WriteString (c .str )
109202}
110203
111- var _ fmt.Stringer = (* Command )(nil )
204+ func (c * Command ) UnmarshalDagJSON (r io.Reader ) error {
205+ jr := jsg .NewDagJsonReader (r )
206+ str , err := jr .ReadStringOrNull (jsg .MaxLength )
207+ if err != nil {
208+ return err
209+ }
210+ if str == nil {
211+ return nil
212+ }
213+ parsed , err := Parse (* str )
214+ if err != nil {
215+ return err
216+ }
217+ * c = parsed
218+ return nil
219+ }
220+
221+ var (
222+ _ fmt.Stringer = (* Command )(nil )
223+ _ cbg.CBORMarshaler = (* Command )(nil )
224+ _ cbg.CBORUnmarshaler = (* Command )(nil )
225+ )
0 commit comments