forked from ipfs/go-path
-
Notifications
You must be signed in to change notification settings - Fork 3
/
path.go
190 lines (158 loc) · 4.98 KB
/
path.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// Package path contains utilities to work with btfs paths.
package path
import (
"fmt"
"path"
"strings"
cid "github.com/ipfs/go-cid"
)
// A Path represents an btfs content path:
// - /<cid>/path/to/file
// - /btfs/<cid>
// - /btns/<cid>/path/to/folder
// - etc
type Path string
// ^^^
// TODO: debate making this a private struct wrapped in a public interface
// would allow us to control creation, and cache segments.
// FromString safely converts a string type to a Path type.
func FromString(s string) Path {
return Path(s)
}
// FromCid safely converts a cid.Cid type to a Path type.
func FromCid(c cid.Cid) Path {
return Path("/btfs/" + c.String())
}
// Segments returns the different elements of a path
// (elements are delimited by a /).
func (p Path) Segments() []string {
cleaned := path.Clean(string(p))
segments := strings.Split(cleaned, "/")
// Ignore leading slash
if len(segments[0]) == 0 {
segments = segments[1:]
}
return segments
}
// String converts a path to string.
func (p Path) String() string {
return string(p)
}
// IsJustAKey returns true if the path is of the form <key> or /btfs/<key>, or
// /ipld/<key>
func (p Path) IsJustAKey() bool {
parts := p.Segments()
return len(parts) == 2 && (parts[0] == "btfs" || parts[0] == "ipld")
}
// PopLastSegment returns a new Path without its final segment, and the final
// segment, separately. If there is no more to pop (the path is just a key),
// the original path is returned.
func (p Path) PopLastSegment() (Path, string, error) {
if p.IsJustAKey() {
return p, "", nil
}
segs := p.Segments()
newPath, err := ParsePath("/" + strings.Join(segs[:len(segs)-1], "/"))
if err != nil {
return "", "", err
}
return newPath, segs[len(segs)-1], nil
}
// FromSegments returns a path given its different segments.
func FromSegments(prefix string, seg ...string) (Path, error) {
return ParsePath(prefix + strings.Join(seg, "/"))
}
// ParsePath returns a well-formed btfs Path.
// The returned path will always be prefixed with /btfs/ or /btns/.
// The prefix will be added if not present in the given string.
// This function will return an error when the given string is
// not a valid btfs path.
func ParsePath(txt string) (Path, error) {
parts := strings.Split(txt, "/")
if len(parts) == 1 {
kp, err := ParseCidToPath(txt)
if err == nil {
return kp, nil
}
}
// if the path doesnt begin with a '/'
// we expect this to start with a hash, and be an 'btfs' path
if parts[0] != "" {
if _, err := decodeCid(parts[0]); err != nil {
return "", &ErrInvalidPath{error: err, path: txt}
}
// The case when the path starts with hash without a protocol prefix
return Path("/btfs/" + txt), nil
}
if len(parts) < 3 {
return "", &ErrInvalidPath{error: fmt.Errorf("invalid btfs path"), path: txt}
}
//TODO: make this smarter
switch parts[1] {
case "btfs", "ipld":
if parts[2] == "" {
return "", &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: txt}
}
// Validate Cid.
_, err := decodeCid(parts[2])
if err != nil {
return "", &ErrInvalidPath{error: fmt.Errorf("invalid CID: %w", err), path: txt}
}
case "btns":
if parts[2] == "" {
return "", &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: txt}
}
default:
return "", &ErrInvalidPath{error: fmt.Errorf("unknown namespace %q", parts[1]), path: txt}
}
return Path(txt), nil
}
// ParseCidToPath takes a CID in string form and returns a valid btfs Path.
func ParseCidToPath(txt string) (Path, error) {
if txt == "" {
return "", &ErrInvalidPath{error: fmt.Errorf("empty"), path: txt}
}
c, err := decodeCid(txt)
if err != nil {
return "", &ErrInvalidPath{error: err, path: txt}
}
return FromCid(c), nil
}
// IsValid checks if a path is a valid btfs Path.
func (p *Path) IsValid() error {
_, err := ParsePath(p.String())
return err
}
// Join joins strings slices using /
func Join(pths []string) string {
return strings.Join(pths, "/")
}
// SplitList splits strings usings /
func SplitList(pth string) []string {
return strings.Split(pth, "/")
}
// SplitAbsPath clean up and split fpath. It extracts the first component (which
// must be a Multihash) and return it separately.
func SplitAbsPath(fpath Path) (cid.Cid, []string, error) {
parts := fpath.Segments()
if parts[0] == "btfs" || parts[0] == "ipld" {
parts = parts[1:]
}
// if nothing, bail.
if len(parts) == 0 {
return cid.Cid{}, nil, &ErrInvalidPath{error: fmt.Errorf("empty"), path: string(fpath)}
}
c, err := decodeCid(parts[0])
// first element in the path is a cid
if err != nil {
return cid.Cid{}, nil, &ErrInvalidPath{error: fmt.Errorf("invalid CID: %w", err), path: string(fpath)}
}
return c, parts[1:], nil
}
func decodeCid(cstr string) (cid.Cid, error) {
c, err := cid.Decode(cstr)
if err != nil && len(cstr) == 46 && cstr[:2] == "qm" { // https://github.com/ipfs/go-ipfs/issues/7792
return cid.Cid{}, fmt.Errorf("%v (possible lowercased CIDv0; consider converting to a case-agnostic CIDv1, such as base32)", err)
}
return c, err
}