Skip to content

Commit 368d2e2

Browse files
committed
Consolidate filesystem ops into pkg
Signed-off-by: apostasie <spam_blackhole@farcloser.world>
1 parent 0c2e76b commit 368d2e2

File tree

17 files changed

+261
-126
lines changed

17 files changed

+261
-126
lines changed

cmd/nerdctl/main.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -248,12 +248,12 @@ Config file ($NERDCTL_TOML): %s
248248
}
249249

250250
// Since we store containers' stateful information on the filesystem per namespace, we need namespaces to be
251-
// valid, safe path segments. This is enforced by store.ValidatePathComponent.
251+
// valid, safe path segments.
252252
// Note that the container runtime will further enforce additional restrictions on namespace names
253253
// (containerd treats namespaces as valid identifiers - eg: alphanumericals + dash, starting with a letter)
254254
// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#path-segment-names for
255255
// considerations about path segments identifiers.
256-
if err = store.ValidatePathComponent(globalOptions.Namespace); err != nil {
256+
if err = store.IsFilesystemSafe(globalOptions.Namespace); err != nil {
257257
return err
258258
}
259259
if appNeedsRootlessParentMain(cmd, args) {

pkg/composer/lock.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
"os"
2121

2222
"github.com/containerd/nerdctl/v2/pkg/clientutil"
23-
"github.com/containerd/nerdctl/v2/pkg/lockutil"
23+
"github.com/containerd/nerdctl/v2/pkg/internal/filesystem"
2424
)
2525

2626
//nolint:unused
@@ -39,10 +39,10 @@ func Lock(dataRoot string, address string) error {
3939
if err != nil {
4040
return err
4141
}
42-
locked, err = lockutil.Lock(dataStore)
42+
locked, err = filesystem.Lock(dataStore)
4343
return err
4444
}
4545

4646
func Unlock() error {
47-
return lockutil.Unlock(locked)
47+
return filesystem.Unlock(locked)
4848
}

pkg/internal/filesystem/atomic.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package filesystem
18+
19+
import (
20+
"os"
21+
"path/filepath"
22+
)
23+
24+
func AtomicWrite(parent string, fileName string, perm os.FileMode, data []byte) error {
25+
dest := filepath.Join(parent, fileName)
26+
temp := filepath.Join(parent, ".temp."+fileName)
27+
28+
err := os.WriteFile(temp, data, perm)
29+
if err != nil {
30+
return err
31+
}
32+
33+
err = os.Rename(temp, dest)
34+
if err != nil {
35+
return err
36+
}
37+
38+
return nil
39+
}

pkg/internal/filesystem/consts.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package filesystem
18+
19+
const (
20+
pathComponentMaxLength = 255
21+
)

pkg/internal/filesystem/errors.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package filesystem
18+
19+
import "errors"
20+
21+
var (
22+
ErrInvalidPath = errors.New("invalid path")
23+
)

pkg/lockutil/lockutil_unix.go renamed to pkg/internal/filesystem/lockutil_unix.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
limitations under the License.
1717
*/
1818

19-
package lockutil
19+
package filesystem
2020

2121
import (
2222
"fmt"

pkg/lockutil/lockutil_windows.go renamed to pkg/internal/filesystem/lockutil_windows.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
limitations under the License.
1515
*/
1616

17-
package lockutil
17+
package filesystem
1818

1919
import (
2020
"fmt"

pkg/internal/filesystem/path.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package filesystem
18+
19+
import (
20+
"errors"
21+
"strings"
22+
)
23+
24+
var (
25+
errForbiddenChars = errors.New("forbidden characters in path component")
26+
errForbiddenKeywords = errors.New("forbidden keywords in path component")
27+
errInvalidPathTooLong = errors.New("path component must be shorter than 256 characters")
28+
errInvalidPathEmpty = errors.New("path component cannot be empty")
29+
)
30+
31+
// ValidatePathComponent will enforce os specific filename restrictions on a single path component.
32+
func ValidatePathComponent(pathComponent string) error {
33+
// https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits
34+
if len(pathComponent) > pathComponentMaxLength {
35+
return errors.Join(ErrInvalidPath, errInvalidPathTooLong)
36+
}
37+
38+
if strings.TrimSpace(pathComponent) == "" {
39+
return errors.Join(ErrInvalidPath, errInvalidPathEmpty)
40+
}
41+
42+
if err := validatePlatformSpecific(pathComponent); err != nil {
43+
return errors.Join(ErrInvalidPath, err)
44+
}
45+
46+
return nil
47+
}

pkg/internal/filesystem/path_test.go

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package filesystem_test
18+
19+
import (
20+
"fmt"
21+
"runtime"
22+
"testing"
23+
24+
"gotest.tools/v3/assert"
25+
26+
"github.com/containerd/nerdctl/v2/pkg/internal/filesystem"
27+
)
28+
29+
func TestFilesystemRestrictions(t *testing.T) {
30+
t.Parallel()
31+
32+
invalid := []string{
33+
"/",
34+
"/start",
35+
"mid/dle",
36+
"end/",
37+
".",
38+
"..",
39+
"",
40+
fmt.Sprintf("A%0255s", "A"),
41+
}
42+
43+
valid := []string{
44+
fmt.Sprintf("A%0254s", "A"),
45+
"test",
46+
"test-hyphen",
47+
".start.dot",
48+
"mid.dot",
49+
"∞",
50+
}
51+
52+
if runtime.GOOS == "windows" {
53+
invalid = append(invalid, []string{
54+
"\\start",
55+
"mid\\dle",
56+
"end\\",
57+
"\\",
58+
"\\.",
59+
"com².whatever",
60+
"lpT2",
61+
"Prn.",
62+
"nUl",
63+
"AUX",
64+
"A<A",
65+
"A>A",
66+
"A:A",
67+
"A\"A",
68+
"A|A",
69+
"A?A",
70+
"A*A",
71+
"end.dot.",
72+
"end.space ",
73+
}...)
74+
}
75+
76+
for _, v := range invalid {
77+
err := filesystem.ValidatePathComponent(v)
78+
assert.ErrorIs(t, err, filesystem.ErrInvalidPath, v)
79+
}
80+
81+
for _, v := range valid {
82+
err := filesystem.ValidatePathComponent(v)
83+
assert.NilError(t, err, v)
84+
}
85+
}

pkg/store/filestore_unix.go renamed to pkg/internal/filesystem/path_unix.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
limitations under the License.
1717
*/
1818

19-
package store
19+
package filesystem
2020

2121
import (
2222
"fmt"
2323
"regexp"
2424
)
2525

26-
// Note that Darwin has different restrictions - though, we do not support Darwin at this point...
26+
// Note that Darwin has different restrictions on colons.
2727
// https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
2828
var (
2929
disallowedKeywords = regexp.MustCompile(`^([.]|[.][.])$`)
@@ -32,11 +32,11 @@ var (
3232

3333
func validatePlatformSpecific(pathComponent string) error {
3434
if reservedCharacters.MatchString(pathComponent) {
35-
return fmt.Errorf("identifier %q cannot contain any of the following characters: %q", pathComponent, reservedCharacters)
35+
return fmt.Errorf("%w: %q (%q)", errForbiddenChars, pathComponent, reservedCharacters)
3636
}
3737

3838
if disallowedKeywords.MatchString(pathComponent) {
39-
return fmt.Errorf("identifier %q cannot be any of the reserved keywords: %q", pathComponent, disallowedKeywords)
39+
return fmt.Errorf("%w: %q (%q)", errForbiddenKeywords, pathComponent, disallowedKeywords)
4040
}
4141

4242
return nil

pkg/store/filestore_windows.go renamed to pkg/internal/filesystem/path_windows.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
limitations under the License.
1515
*/
1616

17-
package store
17+
package filesystem
1818

1919
import (
20+
"errors"
2021
"fmt"
2122
"regexp"
2223
)
@@ -26,19 +27,21 @@ import (
2627
var (
2728
disallowedKeywords = regexp.MustCompile(`(?i)^(con|prn|nul|aux|com[1-9¹²³]|lpt[1-9¹²³])([.].*)?$`)
2829
reservedCharacters = regexp.MustCompile(`[\x{0}-\x{1f}<>:"/\\|?*]`)
30+
31+
errNoEndingSpaceDot = errors.New("component cannot end with a space or dot")
2932
)
3033

3134
func validatePlatformSpecific(pathComponent string) error {
3235
if reservedCharacters.MatchString(pathComponent) {
33-
return fmt.Errorf("identifier %q cannot contain any of the following characters: %q", pathComponent, reservedCharacters)
36+
return fmt.Errorf("%w: %q (%q)", errForbiddenChars, pathComponent, reservedCharacters)
3437
}
3538

3639
if disallowedKeywords.MatchString(pathComponent) {
37-
return fmt.Errorf("identifier %q cannot be any of the reserved keywords: %q", pathComponent, disallowedKeywords)
40+
return fmt.Errorf("%w: %q (%q)", errForbiddenKeywords, pathComponent, disallowedKeywords)
3841
}
3942

4043
if pathComponent[len(pathComponent)-1:] == "." || pathComponent[len(pathComponent)-1:] == " " {
41-
return fmt.Errorf("identifier %q cannot end with a space or dot", pathComponent)
44+
return fmt.Errorf("%w: %q", errNoEndingSpaceDot, pathComponent)
4245
}
4346

4447
return nil

pkg/logging/logging.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import (
3838
"github.com/containerd/errdefs"
3939
"github.com/containerd/log"
4040

41-
"github.com/containerd/nerdctl/v2/pkg/lockutil"
41+
"github.com/containerd/nerdctl/v2/pkg/internal/filesystem"
4242
)
4343

4444
const (
@@ -160,7 +160,7 @@ func getLockPath(dataStore, ns, id string) string {
160160

161161
// WaitForLogger waits until the logger has finished executing and processing container logs
162162
func WaitForLogger(dataStore, ns, id string) error {
163-
return lockutil.WithDirLock(getLockPath(dataStore, ns, id), func() error {
163+
return filesystem.WithDirLock(getLockPath(dataStore, ns, id), func() error {
164164
return nil
165165
})
166166
}
@@ -314,7 +314,7 @@ func loggerFunc(dataStore string) (logging.LoggerFunc, error) {
314314
// the logger will obtain an exclusive lock on a file until the container is
315315
// stopped and the driver has finished processing all output,
316316
// so that waiting log viewers can be signalled when the process is complete.
317-
return lockutil.WithDirLock(loggerLock, func() error {
317+
return filesystem.WithDirLock(loggerLock, func() error {
318318
if err := ready(); err != nil {
319319
return err
320320
}

0 commit comments

Comments
 (0)