Skip to content
Open
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
1 change: 1 addition & 0 deletions cmd/publisher/commands/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func PublishCommand(args []string) error {
}
return fmt.Errorf("failed to read server.json: %w", err)
}
serverData = stripUTF8BOM(serverData)
if err := validateJSONUnicode(serverFile, serverData); err != nil {
return err
}
Expand Down
14 changes: 14 additions & 0 deletions cmd/publisher/commands/publish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,20 @@ func TestPublishCommand_RejectsInvalidUTF8ServerJSON(t *testing.T) {
assert.Contains(t, err.Error(), "UTF-8")
}

func TestPublishCommand_AcceptsUTF8BOM(t *testing.T) {
// Windows tools such as PowerShell's Out-File write UTF-8 with a leading
// byte order mark; RFC 8259 permits parsers to ignore it.
server := SetupMockRegistryServer(t, nil, nil)
SetupTestToken(t, server.URL, "test-token")

bom := []byte{0xEF, 0xBB, 0xBF}
body := []byte(`{"$schema":"` + model.CurrentSchemaURL + `","name":"com.example/test","description":"A test server","version":"1.0.0"}`)
createRawServerJSON(t, append(bom, body...))

err := commands.PublishCommand([]string{})
assert.NoError(t, err)
}

func TestPublishCommand_RejectsUnpairedSurrogateEscape(t *testing.T) {
createRawServerJSON(t, []byte(`{"$schema":"","name":"com.example/test","description":"bad \udc94","version":"1.0.0"}`))

Expand Down
11 changes: 11 additions & 0 deletions cmd/publisher/commands/unicode.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ import (
"unicode/utf8"
)

// utf8BOM is the UTF-8 byte order mark. Some Windows tools (e.g. PowerShell's
// Out-File) prepend it when writing UTF-8 files. RFC 8259 permits JSON parsers
// to ignore it, but Go's encoding/json rejects it, so it is stripped before
// parsing.
var utf8BOM = []byte{0xEF, 0xBB, 0xBF}

// stripUTF8BOM returns data without a leading UTF-8 byte order mark.
func stripUTF8BOM(data []byte) []byte {
return bytes.TrimPrefix(data, utf8BOM)
}

func validateJSONUnicode(filename string, data []byte) error {
if !utf8.Valid(data) {
return fmt.Errorf("%s must be encoded as UTF-8", filename)
Expand Down
1 change: 1 addition & 0 deletions cmd/publisher/commands/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ func ValidateCommand(args []string) error {
}
return fmt.Errorf("failed to read %s: %w", serverFile, err)
}
serverData = stripUTF8BOM(serverData)
if err := validateJSONUnicode(serverFile, serverData); err != nil {
return err
}
Expand Down
14 changes: 14 additions & 0 deletions cmd/publisher/commands/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,20 @@ func TestValidateCommand_DeprecatedSchema(t *testing.T) {
assert.Contains(t, err.Error(), "Migration checklist:")
}

func TestValidateCommand_AcceptsUTF8BOM(t *testing.T) {
// Windows tools such as PowerShell's Out-File write UTF-8 with a leading
// byte order mark; RFC 8259 permits parsers to ignore it.
server := SetupMockRegistryServer(t, nil, nil)
SetupTestToken(t, server.URL, "test-token")

bom := []byte{0xEF, 0xBB, 0xBF}
body := []byte(`{"$schema":"` + model.CurrentSchemaURL + `","name":"com.example/test","description":"A test server","version":"1.0.0"}`)
createRawServerJSON(t, append(bom, body...))

err := commands.ValidateCommand([]string{})
assert.NoError(t, err)
}

func TestValidateCommand_NoServerFile(t *testing.T) {
server := SetupMockRegistryServer(t, nil, nil)
SetupTestToken(t, server.URL, "test-token")
Expand Down
Loading