account for non-import shapes (#3997)

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2025-06-11 13:11:40 -04:00 committed by GitHub
parent 79b6d5daa4
commit 96c34ffc43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 123 additions and 56 deletions

View File

@ -28,7 +28,7 @@ func (d *Document) UnmarshalJSON(data []byte) error {
if d.Schema.Version == "1.0.0" && d.Descriptor.Name == "anchorectl" { if d.Schema.Version == "1.0.0" && d.Descriptor.Name == "anchorectl" {
// convert all file modes from decimal to octal // convert all file modes from decimal to octal
for i := range d.Files { for i := range d.Files {
d.Files[i].Metadata.Mode = convertFileModeToBase8(d.Files[i].Metadata.Mode) d.Files[i].Metadata.Mode = convertBase10ToBase8(d.Files[i].Metadata.Mode)
} }
} }

View File

@ -31,14 +31,21 @@ type FileMetadataEntry struct {
Size int64 `json:"size"` Size int64 `json:"size"`
} }
func (f *FileMetadataEntry) UnmarshalJSON(data []byte) error { type auxFileMetadataEntry FileMetadataEntry
type Alias FileMetadataEntry type fileMetadataEntryWithLegacyHint struct {
aux := (*Alias)(f) *auxFileMetadataEntry `json:",inline"`
LegacyHint any `json:"FileInfo"`
}
if err := json.Unmarshal(data, aux); err == nil { func (f *FileMetadataEntry) UnmarshalJSON(data []byte) error {
// we should have at least one field set to a non-zero value... otherwise this is a legacy entry aux := fileMetadataEntryWithLegacyHint{
if f.Mode != 0 || f.Type != "" || f.LinkDestination != "" || auxFileMetadataEntry: (*auxFileMetadataEntry)(f),
f.UserID != 0 || f.GroupID != 0 || f.MIMEType != "" || f.Size != 0 { }
if err := json.Unmarshal(data, &aux); err == nil {
fieldsSpecified := f.Mode != 0 || f.Type != "" || f.LinkDestination != "" ||
f.UserID != 0 || f.GroupID != 0 || f.MIMEType != "" || f.Size != 0
if aux.LegacyHint == nil && fieldsSpecified {
// we should have at least one field set to a non-zero value... (this is not a legacy shape)
return nil return nil
} }
} }
@ -48,8 +55,14 @@ func (f *FileMetadataEntry) UnmarshalJSON(data []byte) error {
return err return err
} }
if !legacy.Type.WasInt {
// this occurs for document shapes from a non-import path and indicates that the mode has already been converted to octal.
// That being said, we want to handle all legacy shapes the same, so we will convert this to base 10 for consistency.
legacy.Mode = convertBase8ToBase10(legacy.Mode)
}
f.Mode = legacy.Mode f.Mode = legacy.Mode
f.Type = string(legacy.Type) f.Type = legacy.Type.Value
f.LinkDestination = legacy.LinkDestination f.LinkDestination = legacy.LinkDestination
f.UserID = legacy.UserID f.UserID = legacy.UserID
f.GroupID = legacy.GroupID f.GroupID = legacy.GroupID
@ -82,12 +95,15 @@ type FileLicenseEvidence struct {
Extent int `json:"extent"` Extent int `json:"extent"`
} }
type intOrStringFileType string type intOrStringFileType struct {
Value string
WasInt bool
}
func (lt *intOrStringFileType) UnmarshalJSON(data []byte) error { func (lt *intOrStringFileType) UnmarshalJSON(data []byte) error {
var str string var str string
if err := json.Unmarshal(data, &str); err == nil { if err := json.Unmarshal(data, &str); err == nil {
*lt = intOrStringFileType(str) lt.Value = str
return nil return nil
} }
@ -96,13 +112,21 @@ func (lt *intOrStringFileType) UnmarshalJSON(data []byte) error {
return fmt.Errorf("file.Type must be either string or int, got: %s", string(data)) return fmt.Errorf("file.Type must be either string or int, got: %s", string(data))
} }
*lt = intOrStringFileType(num.String()) lt.Value = num.String()
lt.WasInt = true
return nil return nil
} }
func convertFileModeToBase8(rawMode int) int { func convertBase10ToBase8(rawMode int) int {
octalStr := fmt.Sprintf("%o", rawMode) octalStr := fmt.Sprintf("%o", rawMode)
// we don't need to check that this is a valid octal string since the input is always an integer // we don't need to check that this is a valid octal string since the input is always an integer
result, _ := strconv.Atoi(octalStr) result, _ := strconv.Atoi(octalStr)
return result return result
} }
func convertBase8ToBase10(octalMode int) int {
octalStr := strconv.Itoa(octalMode)
result, _ := strconv.ParseInt(octalStr, 8, 64)
return int(result)
}

View File

@ -36,8 +36,9 @@ func Test_FileMetadataEntry_UnmarshalJSON(t *testing.T) {
}, },
}, },
{ {
name: "unmarshal legacy sbom import format", name: "unmarshal legacy image add internal document format",
jsonData: []byte(`{ jsonData: []byte(`{
"FileInfo": {},
"Mode": 644, "Mode": 644,
"Type": "RegularFile", "Type": "RegularFile",
"LinkDestination": "/usr/bin/python3", "LinkDestination": "/usr/bin/python3",
@ -47,7 +48,7 @@ func Test_FileMetadataEntry_UnmarshalJSON(t *testing.T) {
"Size": 10174 "Size": 10174
}`), }`),
expected: FileMetadataEntry{ expected: FileMetadataEntry{
Mode: 644, Mode: 420, // important! we convert this to base 10 so that all documents are consistent
Type: "RegularFile", Type: "RegularFile",
LinkDestination: "/usr/bin/python3", LinkDestination: "/usr/bin/python3",
UserID: 1000, UserID: 1000,
@ -57,8 +58,9 @@ func Test_FileMetadataEntry_UnmarshalJSON(t *testing.T) {
}, },
}, },
{ {
name: "unmarshal legacy sbom import format - integer type", name: "unmarshal legacy sbom import format",
jsonData: []byte(`{ jsonData: []byte(`{
"FileInfo": {},
"Mode": 644, "Mode": 644,
"Type": 0, "Type": 0,
"LinkDestination": "/usr/bin/python3", "LinkDestination": "/usr/bin/python3",
@ -93,6 +95,7 @@ func Test_FileMetadataEntry_UnmarshalJSON(t *testing.T) {
{ {
name: "unmarshal minimal legacy format", name: "unmarshal minimal legacy format",
jsonData: []byte(`{ jsonData: []byte(`{
"FileInfo": {},
"Mode": 0, "Mode": 0,
"Type": "RegularFile", "Type": "RegularFile",
"UserID": 0, "UserID": 0,
@ -123,6 +126,7 @@ func Test_intOrStringFileType_UnmarshalJSON(t *testing.T) {
name string name string
jsonData []byte jsonData []byte
expected string expected string
expectedWasInt bool
wantErr require.ErrorAssertionFunc wantErr require.ErrorAssertionFunc
}{ }{
// string inputs - should pass through unchanged // string inputs - should pass through unchanged
@ -151,56 +155,67 @@ func Test_intOrStringFileType_UnmarshalJSON(t *testing.T) {
name: "int 0 (TypeRegular)", name: "int 0 (TypeRegular)",
jsonData: []byte(`0`), jsonData: []byte(`0`),
expected: "RegularFile", expected: "RegularFile",
expectedWasInt: true,
}, },
{ {
name: "int 1 (TypeHardLink)", name: "int 1 (TypeHardLink)",
jsonData: []byte(`1`), jsonData: []byte(`1`),
expected: "HardLink", expected: "HardLink",
expectedWasInt: true,
}, },
{ {
name: "int 2 (TypeSymLink)", name: "int 2 (TypeSymLink)",
jsonData: []byte(`2`), jsonData: []byte(`2`),
expected: "SymbolicLink", expected: "SymbolicLink",
expectedWasInt: true,
}, },
{ {
name: "int 3 (TypeCharacterDevice)", name: "int 3 (TypeCharacterDevice)",
jsonData: []byte(`3`), jsonData: []byte(`3`),
expected: "CharacterDevice", expected: "CharacterDevice",
expectedWasInt: true,
}, },
{ {
name: "int 4 (TypeBlockDevice)", name: "int 4 (TypeBlockDevice)",
jsonData: []byte(`4`), jsonData: []byte(`4`),
expected: "BlockDevice", expected: "BlockDevice",
expectedWasInt: true,
}, },
{ {
name: "int 5 (TypeDirectory)", name: "int 5 (TypeDirectory)",
jsonData: []byte(`5`), jsonData: []byte(`5`),
expected: "Directory", expected: "Directory",
expectedWasInt: true,
}, },
{ {
name: "int 6 (TypeFIFO)", name: "int 6 (TypeFIFO)",
jsonData: []byte(`6`), jsonData: []byte(`6`),
expected: "FIFONode", expected: "FIFONode",
expectedWasInt: true,
}, },
{ {
name: "int 7 (TypeSocket)", name: "int 7 (TypeSocket)",
jsonData: []byte(`7`), jsonData: []byte(`7`),
expected: "Socket", expected: "Socket",
expectedWasInt: true,
}, },
{ {
name: "int 8 (TypeIrregular)", name: "int 8 (TypeIrregular)",
jsonData: []byte(`8`), jsonData: []byte(`8`),
expected: "IrregularFile", expected: "IrregularFile",
expectedWasInt: true,
}, },
{ {
name: "unknown int", name: "unknown int",
jsonData: []byte(`99`), jsonData: []byte(`99`),
expected: "Unknown", expected: "Unknown",
expectedWasInt: true,
}, },
{ {
name: "negative int", name: "negative int",
jsonData: []byte(`-1`), jsonData: []byte(`-1`),
expected: "Unknown", expected: "Unknown",
expectedWasInt: true,
}, },
{ {
name: "null value", name: "null value",
@ -244,12 +259,13 @@ func Test_intOrStringFileType_UnmarshalJSON(t *testing.T) {
if err != nil { if err != nil {
return return
} }
assert.Equal(t, test.expected, string(ft)) assert.Equal(t, test.expected, ft.Value)
assert.Equal(t, test.expectedWasInt, ft.WasInt)
}) })
} }
} }
func Test_convertFileModeToBase8(t *testing.T) { func Test_convertBase10ToBase8(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input int input int
@ -269,7 +285,34 @@ func Test_convertFileModeToBase8(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
actual := convertFileModeToBase8(tt.input) actual := convertBase10ToBase8(tt.input)
require.Equal(t, tt.expected, actual)
})
}
}
func Test_convertBase8ToBase10(t *testing.T) {
tests := []struct {
name string
input int
expected int
}{
{
name: "no permissions",
input: 0,
expected: 0,
},
{
name: "symlink + rwxrwxrwx",
input: 1000000777,
expected: 134218239,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := convertBase8ToBase10(tt.input)
require.Equal(t, tt.expected, actual) require.Equal(t, tt.expected, actual)
}) })