This cleans up the linting results by adding checks for integer underflow/overflow in several places, suppressing the error in places where it has been checked, or fixing the types when possible.
336 lines
8.7 KiB
Go
336 lines
8.7 KiB
Go
package blueprint
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
|
|
"github.com/osbuild/osbuild-composer/internal/common"
|
|
"github.com/osbuild/osbuild-composer/internal/fsnode"
|
|
)
|
|
|
|
// validateModeString checks that the given string is a valid mode octal number
|
|
func validateModeString(mode string) error {
|
|
// Check that the mode string matches the octal format regular expression.
|
|
// The leading is optional.
|
|
if regexp.MustCompile(`^[0]{0,1}[0-7]{3}$`).MatchString(mode) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("invalid mode %s: must be an octal number", mode)
|
|
}
|
|
|
|
// DirectoryCustomization represents a directory to be created in the image
|
|
type DirectoryCustomization struct {
|
|
// Absolute path to the directory
|
|
Path string `json:"path" toml:"path"`
|
|
// Owner of the directory specified as a string (user name), int64 (UID) or nil
|
|
User interface{} `json:"user,omitempty" toml:"user,omitempty"`
|
|
// Owner of the directory specified as a string (group name), int64 (UID) or nil
|
|
Group interface{} `json:"group,omitempty" toml:"group,omitempty"`
|
|
// Permissions of the directory specified as an octal number
|
|
Mode string `json:"mode,omitempty" toml:"mode,omitempty"`
|
|
// EnsureParents ensures that all parent directories of the directory exist
|
|
EnsureParents bool `json:"ensure_parents,omitempty" toml:"ensure_parents,omitempty"`
|
|
}
|
|
|
|
// Custom TOML unmarshalling for DirectoryCustomization with validation
|
|
func (d *DirectoryCustomization) UnmarshalTOML(data interface{}) error {
|
|
var dir DirectoryCustomization
|
|
|
|
dataMap, _ := data.(map[string]interface{})
|
|
|
|
switch path := dataMap["path"].(type) {
|
|
case string:
|
|
dir.Path = path
|
|
default:
|
|
return fmt.Errorf("UnmarshalTOML: path must be a string")
|
|
}
|
|
|
|
switch user := dataMap["user"].(type) {
|
|
case string:
|
|
dir.User = user
|
|
case int64:
|
|
dir.User = user
|
|
case nil:
|
|
break
|
|
default:
|
|
return fmt.Errorf("UnmarshalTOML: user must be a string or an integer, got %T", user)
|
|
}
|
|
|
|
switch group := dataMap["group"].(type) {
|
|
case string:
|
|
dir.Group = group
|
|
case int64:
|
|
dir.Group = group
|
|
case nil:
|
|
break
|
|
default:
|
|
return fmt.Errorf("UnmarshalTOML: group must be a string or an integer")
|
|
}
|
|
|
|
switch mode := dataMap["mode"].(type) {
|
|
case string:
|
|
dir.Mode = mode
|
|
case nil:
|
|
break
|
|
default:
|
|
return fmt.Errorf("UnmarshalTOML: mode must be a string")
|
|
}
|
|
|
|
switch ensureParents := dataMap["ensure_parents"].(type) {
|
|
case bool:
|
|
dir.EnsureParents = ensureParents
|
|
case nil:
|
|
break
|
|
default:
|
|
return fmt.Errorf("UnmarshalTOML: ensure_parents must be a bool")
|
|
}
|
|
|
|
// try converting to fsnode.Directory to validate all values
|
|
_, err := dir.ToFsNodeDirectory()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
*d = dir
|
|
return nil
|
|
}
|
|
|
|
// Custom JSON unmarshalling for DirectoryCustomization with validation
|
|
func (d *DirectoryCustomization) UnmarshalJSON(data []byte) error {
|
|
type directoryCustomization DirectoryCustomization
|
|
|
|
var dirPrivate directoryCustomization
|
|
if err := json.Unmarshal(data, &dirPrivate); err != nil {
|
|
return err
|
|
}
|
|
|
|
dir := DirectoryCustomization(dirPrivate)
|
|
if uid, ok := dir.User.(float64); ok {
|
|
// check if uid can be converted to int64
|
|
if uid != float64(int64(uid)) {
|
|
return fmt.Errorf("invalid user %f: must be an integer", uid)
|
|
}
|
|
dir.User = int64(uid)
|
|
}
|
|
if gid, ok := dir.Group.(float64); ok {
|
|
// check if gid can be converted to int64
|
|
if gid != float64(int64(gid)) {
|
|
return fmt.Errorf("invalid group %f: must be an integer", gid)
|
|
}
|
|
dir.Group = int64(gid)
|
|
}
|
|
// try converting to fsnode.Directory to validate all values
|
|
_, err := dir.ToFsNodeDirectory()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
*d = dir
|
|
return nil
|
|
}
|
|
|
|
// ToFsNodeDirectory converts the DirectoryCustomization to an fsnode.Directory
|
|
func (d DirectoryCustomization) ToFsNodeDirectory() (*fsnode.Directory, error) {
|
|
var mode *os.FileMode
|
|
if d.Mode != "" {
|
|
err := validateModeString(d.Mode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
modeNum, err := strconv.ParseUint(d.Mode, 8, 32)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid mode %s: %v", d.Mode, err)
|
|
}
|
|
// modeNum is parsed as an unsigned 32 bit int
|
|
/* #nosec G115 */
|
|
mode = common.ToPtr(os.FileMode(modeNum))
|
|
}
|
|
|
|
return fsnode.NewDirectory(d.Path, mode, d.User, d.Group, d.EnsureParents)
|
|
}
|
|
|
|
// DirectoryCustomizationsToFsNodeDirectories converts a slice of DirectoryCustomizations
|
|
// to a slice of fsnode.Directories
|
|
func DirectoryCustomizationsToFsNodeDirectories(dirs []DirectoryCustomization) ([]*fsnode.Directory, error) {
|
|
if len(dirs) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
var fsDirs []*fsnode.Directory
|
|
var errors []error
|
|
for _, dir := range dirs {
|
|
fsDir, err := dir.ToFsNodeDirectory()
|
|
if err != nil {
|
|
errors = append(errors, err)
|
|
}
|
|
fsDirs = append(fsDirs, fsDir)
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
return nil, fmt.Errorf("invalid directory customizations: %v", errors)
|
|
}
|
|
|
|
return fsDirs, nil
|
|
}
|
|
|
|
// FileCustomization represents a file to be created in the image
|
|
type FileCustomization struct {
|
|
// Absolute path to the file
|
|
Path string `json:"path" toml:"path"`
|
|
// Owner of the directory specified as a string (user name), int64 (UID) or nil
|
|
User interface{} `json:"user,omitempty" toml:"user,omitempty"`
|
|
// Owner of the directory specified as a string (group name), int64 (UID) or nil
|
|
Group interface{} `json:"group,omitempty" toml:"group,omitempty"`
|
|
// Permissions of the file specified as an octal number
|
|
Mode string `json:"mode,omitempty" toml:"mode,omitempty"`
|
|
// Data is the file content in plain text
|
|
Data string `json:"data,omitempty" toml:"data,omitempty"`
|
|
}
|
|
|
|
// Custom TOML unmarshalling for FileCustomization with validation
|
|
func (f *FileCustomization) UnmarshalTOML(data interface{}) error {
|
|
var file FileCustomization
|
|
|
|
dataMap, _ := data.(map[string]interface{})
|
|
|
|
switch path := dataMap["path"].(type) {
|
|
case string:
|
|
file.Path = path
|
|
default:
|
|
return fmt.Errorf("UnmarshalTOML: path must be a string")
|
|
}
|
|
|
|
switch user := dataMap["user"].(type) {
|
|
case string:
|
|
file.User = user
|
|
case int64:
|
|
file.User = user
|
|
case nil:
|
|
break
|
|
default:
|
|
return fmt.Errorf("UnmarshalTOML: user must be a string or an integer")
|
|
}
|
|
|
|
switch group := dataMap["group"].(type) {
|
|
case string:
|
|
file.Group = group
|
|
case int64:
|
|
file.Group = group
|
|
case nil:
|
|
break
|
|
default:
|
|
return fmt.Errorf("UnmarshalTOML: group must be a string or an integer")
|
|
}
|
|
|
|
switch mode := dataMap["mode"].(type) {
|
|
case string:
|
|
file.Mode = mode
|
|
case nil:
|
|
break
|
|
default:
|
|
return fmt.Errorf("UnmarshalTOML: mode must be a string")
|
|
}
|
|
|
|
switch data := dataMap["data"].(type) {
|
|
case string:
|
|
file.Data = data
|
|
case nil:
|
|
break
|
|
default:
|
|
return fmt.Errorf("UnmarshalTOML: data must be a string")
|
|
}
|
|
|
|
// try converting to fsnode.File to validate all values
|
|
_, err := file.ToFsNodeFile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
*f = file
|
|
return nil
|
|
}
|
|
|
|
// Custom JSON unmarshalling for FileCustomization with validation
|
|
func (f *FileCustomization) UnmarshalJSON(data []byte) error {
|
|
type fileCustomization FileCustomization
|
|
|
|
var filePrivate fileCustomization
|
|
if err := json.Unmarshal(data, &filePrivate); err != nil {
|
|
return err
|
|
}
|
|
|
|
file := FileCustomization(filePrivate)
|
|
if uid, ok := file.User.(float64); ok {
|
|
// check if uid can be converted to int64
|
|
if uid != float64(int64(uid)) {
|
|
return fmt.Errorf("invalid user %f: must be an integer", uid)
|
|
}
|
|
file.User = int64(uid)
|
|
}
|
|
if gid, ok := file.Group.(float64); ok {
|
|
// check if gid can be converted to int64
|
|
if gid != float64(int64(gid)) {
|
|
return fmt.Errorf("invalid group %f: must be an integer", gid)
|
|
}
|
|
file.Group = int64(gid)
|
|
}
|
|
// try converting to fsnode.File to validate all values
|
|
_, err := file.ToFsNodeFile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
*f = file
|
|
return nil
|
|
}
|
|
|
|
// ToFsNodeFile converts the FileCustomization to an fsnode.File
|
|
func (f FileCustomization) ToFsNodeFile() (*fsnode.File, error) {
|
|
var data []byte
|
|
if f.Data != "" {
|
|
data = []byte(f.Data)
|
|
}
|
|
|
|
var mode *os.FileMode
|
|
if f.Mode != "" {
|
|
err := validateModeString(f.Mode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
modeNum, err := strconv.ParseUint(f.Mode, 8, 32)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid mode %s: %v", f.Mode, err)
|
|
}
|
|
// modeNum is parsed as an unsigned 32 bit int
|
|
/* #nosec G115 */
|
|
mode = common.ToPtr(os.FileMode(modeNum))
|
|
}
|
|
|
|
return fsnode.NewFile(f.Path, mode, f.User, f.Group, data)
|
|
}
|
|
|
|
// FileCustomizationsToFsNodeFiles converts a slice of FileCustomization to a slice of *fsnode.File
|
|
func FileCustomizationsToFsNodeFiles(files []FileCustomization) ([]*fsnode.File, error) {
|
|
if len(files) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
var fsFiles []*fsnode.File
|
|
var errors []error
|
|
for _, file := range files {
|
|
fsFile, err := file.ToFsNodeFile()
|
|
if err != nil {
|
|
errors = append(errors, err)
|
|
}
|
|
fsFiles = append(fsFiles, fsFile)
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
return nil, fmt.Errorf("invalid file customizations: %v", errors)
|
|
}
|
|
|
|
return fsFiles, nil
|
|
}
|