Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions shared_directory.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package vz
import (
"fmt"
"os"
"strings"
"unicode/utf8"
"unsafe"

"github.com/Code-Hex/vz/v4/internal/objc"
Expand Down Expand Up @@ -126,6 +128,43 @@ type MultipleDirectoryShare struct {

var _ DirectoryShare = (*MultipleDirectoryShare)(nil)

// CanonicalizeDirectoryShareName applies Apple's directory share name conversion.
// It removes *, /, :, <, >, ?, \, and |, and discards the first NUL byte and
// everything after it. It returns an error if name contains invalid UTF-8,
// or if the filtered name is empty, is "." or "..", or exceeds 255 UTF-8 bytes.
//
// After these checks, it removes one leading byte order mark (U+FEFF), matching
// Apple's string conversion. This can leave an empty name, ".", or "..".
// NewMultipleDirectoryShare rejects those results.
// Other characters, including whitespace and combining characters, are preserved.
// Different inputs can produce the same name; callers must handle collisions
// before adding names to a share map.
//
// This function runs entirely in Go and does not require macOS 12.
//
// see: https://developer.apple.com/documentation/virtualization/vzmultipledirectoryshare/canonicalizedname(from:)?language=objc
func CanonicalizeDirectoryShareName(name string) (string, error) {
if !utf8.ValidString(name) {
return "", fmt.Errorf("directory share name must be valid UTF-8")
}
name, _, _ = strings.Cut(name, "\x00")
name = strings.Map(func(r rune) rune {
switch r {
case '*', '/', ':', '<', '>', '?', '\\', '|':
return -1
}
return r
}, name)
switch name {
case "", ".", "..":
return "", fmt.Errorf("cannot produce a valid directory share name")
}
if len(name) > 255 {
return "", fmt.Errorf("directory share name exceeds 255 UTF-8 bytes")
}
return strings.TrimPrefix(name, "\uFEFF"), nil
}

// NewMultipleDirectoryShare creates a new multiple directories share.
// It returns an error if a directory name is invalid or a directory is nil or uninitialized.
//
Expand Down
49 changes: 49 additions & 0 deletions shared_directory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,52 @@ func TestDirectoryShareInvalidDirectory(t *testing.T) {
})
}
}

func TestCanonicalizeDirectoryShareName(t *testing.T) {
cases := []struct {
name string
input string
want string
wantErr bool
}{
{name: "unchanged", input: "Shared Documents", want: "Shared Documents"},
{name: "unsafe characters", input: `a*/:<>?\|b`, want: "ab"},
{name: "preserved punctuation", input: " .hidden\t\n\" ", want: " .hidden\t\n\" "},
{name: "unicode", input: "日本語😀e\u0301", want: "日本語😀e\u0301"},
{name: "leading BOM", input: "\uFEFFname", want: "name"},
{name: "BOM after unsafe character", input: "/\uFEFFname", want: "name"},
{name: "two leading BOMs", input: "\uFEFF\uFEFFname", want: "\uFEFFname"},
{name: "internal BOM", input: "a\uFEFFb", want: "a\uFEFFb"},
{name: "BOM becomes empty", input: "\uFEFF", want: ""},
{name: "BOM becomes dot", input: "/\uFEFF.", want: "."},
{name: "BOM becomes dot dot", input: "\uFEFF..", want: ".."},
{name: "maximum bytes with BOM", input: "\uFEFF" + strings.Repeat("a", 252), want: strings.Repeat("a", 252)},
{name: "too long before BOM removal", input: "\uFEFF" + strings.Repeat("a", 253), wantErr: true},
{name: "empty", wantErr: true},
{name: "only unsafe characters", input: `*/:<>?\|`, wantErr: true},
{name: "dot", input: "/.", wantErr: true},
{name: "dot dot", input: ".:/.", wantErr: true},
{name: "three dots", input: "...", want: "..."},
{name: "maximum bytes", input: strings.Repeat("a", 255), want: strings.Repeat("a", 255)},
{name: "too long", input: strings.Repeat("a", 256), wantErr: true},
{name: "multibyte maximum", input: strings.Repeat("😀", 63) + "abc", want: strings.Repeat("😀", 63) + "abc"},
{name: "multibyte too long", input: strings.Repeat("😀", 64), wantErr: true},
{name: "length after removal", input: strings.Repeat("/", 256) + "name", want: "name"},
{name: "invalid UTF-8", input: "a\xffb", wantErr: true},
{name: "NUL", input: "a\x00b", want: "a"},
{name: "leading NUL", input: "\x00a", wantErr: true},
{name: "NUL after unsafe characters", input: "a/\x00b", want: "a"},
{name: "NUL before unsafe characters", input: "a\x00/b", want: "a"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := vz.CanonicalizeDirectoryShareName(tc.input)
if (err != nil) != tc.wantErr {
t.Fatalf("error = %v, want error = %t", err, tc.wantErr)
}
if got != tc.want {
t.Fatalf("name = %q, want %q", got, tc.want)
}
})
}
}
Loading