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" {
// convert all file modes from decimal to octal
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"`
}
func (f *FileMetadataEntry) UnmarshalJSON(data []byte) error {
type Alias FileMetadataEntry
aux := (*Alias)(f)
type auxFileMetadataEntry FileMetadataEntry
type fileMetadataEntryWithLegacyHint struct {
*auxFileMetadataEntry `json:",inline"`
LegacyHint any `json:"FileInfo"`
}
if err := json.Unmarshal(data, aux); err == nil {
// we should have at least one field set to a non-zero value... otherwise this is a legacy entry
if f.Mode != 0 || f.Type != "" || f.LinkDestination != "" ||
f.UserID != 0 || f.GroupID != 0 || f.MIMEType != "" || f.Size != 0 {
func (f *FileMetadataEntry) UnmarshalJSON(data []byte) error {
aux := fileMetadataEntryWithLegacyHint{
auxFileMetadataEntry: (*auxFileMetadataEntry)(f),
}
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
}
}
@ -48,8 +55,14 @@ func (f *FileMetadataEntry) UnmarshalJSON(data []byte) error {
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.Type = string(legacy.Type)
f.Type = legacy.Type.Value
f.LinkDestination = legacy.LinkDestination
f.UserID = legacy.UserID
f.GroupID = legacy.GroupID
@ -82,12 +95,15 @@ type FileLicenseEvidence struct {
Extent int `json:"extent"`
}
type intOrStringFileType string
type intOrStringFileType struct {
Value string
WasInt bool
}
func (lt *intOrStringFileType) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err == nil {
*lt = intOrStringFileType(str)
lt.Value = str
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))
}
*lt = intOrStringFileType(num.String())
lt.Value = num.String()
lt.WasInt = true
return nil
}
func convertFileModeToBase8(rawMode int) int {
func convertBase10ToBase8(rawMode int) int {
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
result, _ := strconv.Atoi(octalStr)
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(`{
"FileInfo": {},
"Mode": 644,
"Type": "RegularFile",
"LinkDestination": "/usr/bin/python3",
@ -47,7 +48,7 @@ func Test_FileMetadataEntry_UnmarshalJSON(t *testing.T) {
"Size": 10174
}`),
expected: FileMetadataEntry{
Mode: 644,
Mode: 420, // important! we convert this to base 10 so that all documents are consistent
Type: "RegularFile",
LinkDestination: "/usr/bin/python3",
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(`{
"FileInfo": {},
"Mode": 644,
"Type": 0,
"LinkDestination": "/usr/bin/python3",
@ -93,6 +95,7 @@ func Test_FileMetadataEntry_UnmarshalJSON(t *testing.T) {
{
name: "unmarshal minimal legacy format",
jsonData: []byte(`{
"FileInfo": {},
"Mode": 0,
"Type": "RegularFile",
"UserID": 0,
@ -120,10 +123,11 @@ func Test_FileMetadataEntry_UnmarshalJSON(t *testing.T) {
func Test_intOrStringFileType_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
jsonData []byte
expected string
wantErr require.ErrorAssertionFunc
name string
jsonData []byte
expected string
expectedWasInt bool
wantErr require.ErrorAssertionFunc
}{
// string inputs - should pass through unchanged
{
@ -148,59 +152,70 @@ func Test_intOrStringFileType_UnmarshalJSON(t *testing.T) {
},
// integer inputs - should convert to string representation
{
name: "int 0 (TypeRegular)",
jsonData: []byte(`0`),
expected: "RegularFile",
name: "int 0 (TypeRegular)",
jsonData: []byte(`0`),
expected: "RegularFile",
expectedWasInt: true,
},
{
name: "int 1 (TypeHardLink)",
jsonData: []byte(`1`),
expected: "HardLink",
name: "int 1 (TypeHardLink)",
jsonData: []byte(`1`),
expected: "HardLink",
expectedWasInt: true,
},
{
name: "int 2 (TypeSymLink)",
jsonData: []byte(`2`),
expected: "SymbolicLink",
name: "int 2 (TypeSymLink)",
jsonData: []byte(`2`),
expected: "SymbolicLink",
expectedWasInt: true,
},
{
name: "int 3 (TypeCharacterDevice)",
jsonData: []byte(`3`),
expected: "CharacterDevice",
name: "int 3 (TypeCharacterDevice)",
jsonData: []byte(`3`),
expected: "CharacterDevice",
expectedWasInt: true,
},
{
name: "int 4 (TypeBlockDevice)",
jsonData: []byte(`4`),
expected: "BlockDevice",
name: "int 4 (TypeBlockDevice)",
jsonData: []byte(`4`),
expected: "BlockDevice",
expectedWasInt: true,
},
{
name: "int 5 (TypeDirectory)",
jsonData: []byte(`5`),
expected: "Directory",
name: "int 5 (TypeDirectory)",
jsonData: []byte(`5`),
expected: "Directory",
expectedWasInt: true,
},
{
name: "int 6 (TypeFIFO)",
jsonData: []byte(`6`),
expected: "FIFONode",
name: "int 6 (TypeFIFO)",
jsonData: []byte(`6`),
expected: "FIFONode",
expectedWasInt: true,
},
{
name: "int 7 (TypeSocket)",
jsonData: []byte(`7`),
expected: "Socket",
name: "int 7 (TypeSocket)",
jsonData: []byte(`7`),
expected: "Socket",
expectedWasInt: true,
},
{
name: "int 8 (TypeIrregular)",
jsonData: []byte(`8`),
expected: "IrregularFile",
name: "int 8 (TypeIrregular)",
jsonData: []byte(`8`),
expected: "IrregularFile",
expectedWasInt: true,
},
{
name: "unknown int",
jsonData: []byte(`99`),
expected: "Unknown",
name: "unknown int",
jsonData: []byte(`99`),
expected: "Unknown",
expectedWasInt: true,
},
{
name: "negative int",
jsonData: []byte(`-1`),
expected: "Unknown",
name: "negative int",
jsonData: []byte(`-1`),
expected: "Unknown",
expectedWasInt: true,
},
{
name: "null value",
@ -244,12 +259,13 @@ func Test_intOrStringFileType_UnmarshalJSON(t *testing.T) {
if err != nil {
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 {
name string
input int
@ -269,7 +285,34 @@ func Test_convertFileModeToBase8(t *testing.T) {
for _, tt := range tests {
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)
})