Skip to content

Commit 5e24548

Browse files
authored
Merge pull request moby#3946 from tonistiigi/file-path-fix
filesync: fix handling non-ascii in file paths
2 parents 7338fab + a09e2d8 commit 5e24548

File tree

2 files changed

+102
-0
lines changed

2 files changed

+102
-0
lines changed

frontend/dockerfile/dockerfile_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ var allTests = integration.TestFuncs(
168168
testSBOMScannerArgs,
169169
testMultiPlatformWarnings,
170170
testNilContextInSolveGateway,
171+
testCopyUnicodePath,
171172
)
172173

173174
// Tests that depend on the `security.*` entitlements
@@ -6734,6 +6735,46 @@ func testNilContextInSolveGateway(t *testing.T, sb integration.Sandbox) {
67346735
require.ErrorContains(t, err, "invalid nil input definition to definition op")
67356736
}
67366737

6738+
func testCopyUnicodePath(t *testing.T, sb integration.Sandbox) {
6739+
f := getFrontend(t, sb)
6740+
c, err := client.New(sb.Context(), sb.Address())
6741+
require.NoError(t, err)
6742+
defer c.Close()
6743+
6744+
dockerfile := []byte(`
6745+
FROM alpine
6746+
COPY test-äöü.txt /
6747+
`)
6748+
6749+
dir, err := integration.Tmpdir(
6750+
t,
6751+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
6752+
fstest.CreateFile("test-äöü.txt", []byte("test"), 0644),
6753+
)
6754+
require.NoError(t, err)
6755+
6756+
destDir, err := integration.Tmpdir(t)
6757+
require.NoError(t, err)
6758+
6759+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
6760+
Exports: []client.ExportEntry{
6761+
{
6762+
Type: client.ExporterLocal,
6763+
OutputDir: destDir,
6764+
},
6765+
},
6766+
LocalDirs: map[string]string{
6767+
dockerui.DefaultLocalNameDockerfile: dir,
6768+
dockerui.DefaultLocalNameContext: dir,
6769+
},
6770+
}, nil)
6771+
require.NoError(t, err)
6772+
6773+
dt, err := os.ReadFile(filepath.Join(destDir, "test-äöü.txt"))
6774+
require.NoError(t, err)
6775+
require.Equal(t, "test", string(dt))
6776+
}
6777+
67376778
func runShell(dir string, cmds ...string) error {
67386779
for _, args := range cmds {
67396780
var cmd *exec.Cmd

session/filesync/filesync.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import (
44
"context"
55
"fmt"
66
io "io"
7+
"net/url"
78
"os"
9+
"strconv"
810
"strings"
11+
"unicode"
912

1013
"github.com/moby/buildkit/session"
1114
"github.com/pkg/errors"
@@ -24,6 +27,7 @@ const (
2427
keyFollowPaths = "followpaths"
2528
keyDirName = "dir-name"
2629
keyExporterMetaPrefix = "exporter-md-"
30+
keyOptsEncoded = "opts-encoded"
2731
)
2832

2933
type fsSyncProvider struct {
@@ -83,6 +87,17 @@ func (sp *fsSyncProvider) handle(method string, stream grpc.ServerStream) (retEr
8387

8488
opts, _ := metadata.FromIncomingContext(stream.Context()) // if no metadata continue with empty object
8589

90+
isDecoded := false
91+
if v, ok := opts[keyOptsEncoded]; ok && len(v) > 0 {
92+
if b, _ := strconv.ParseBool(v[0]); b {
93+
isDecoded = true
94+
}
95+
}
96+
97+
if isDecoded {
98+
opts = decodeOpts(opts)
99+
}
100+
86101
dirName := ""
87102
name, ok := opts[keyDirName]
88103
if ok && len(name) > 0 {
@@ -209,6 +224,11 @@ func FSSync(ctx context.Context, c session.Caller, opt FSSendRequestOpt) error {
209224

210225
var stream grpc.ClientStream
211226

227+
// mark that we have encoded options so older versions with raw values can be detected on client side
228+
opts[keyOptsEncoded] = []string{"1"}
229+
230+
opts = encodeOpts(opts)
231+
212232
ctx = metadata.NewOutgoingContext(ctx, opts)
213233

214234
switch pr.name {
@@ -337,3 +357,44 @@ func (e InvalidSessionError) Error() string {
337357
func (e InvalidSessionError) Unwrap() error {
338358
return e.err
339359
}
360+
361+
func encodeOpts(opts map[string][]string) map[string][]string {
362+
md := make(map[string][]string, len(opts))
363+
for k, v := range opts {
364+
out := make([]string, len(v))
365+
for i, s := range v {
366+
out[i] = encodeStringForHeader(s)
367+
}
368+
md[k] = out
369+
}
370+
return md
371+
}
372+
373+
func decodeOpts(opts map[string][]string) map[string][]string {
374+
md := make(map[string][]string, len(opts))
375+
for k, v := range opts {
376+
out := make([]string, len(v))
377+
for i, s := range v {
378+
out[i], _ = url.QueryUnescape(s)
379+
}
380+
md[k] = out
381+
}
382+
return md
383+
}
384+
385+
// encodeStringForHeader encodes a string value so it can be used in grpc header. This encoding
386+
// is backwards compatible and avoids encoding ASCII characters.
387+
func encodeStringForHeader(input string) string {
388+
var output strings.Builder
389+
for _, runeVal := range input {
390+
// Only encode non-ASCII characters.
391+
if runeVal > unicode.MaxASCII {
392+
// Encode each non-ASCII character individually.
393+
output.WriteString(url.QueryEscape(string(runeVal)))
394+
} else {
395+
// Directly append ASCII characters and '*' to the output.
396+
output.WriteRune(runeVal)
397+
}
398+
}
399+
return output.String()
400+
}

0 commit comments

Comments
 (0)