Added support for repo generate and some other various improvements

This commit is contained in:
Arzumify 2024-09-01 20:23:37 +01:00
parent 0dbea32590
commit 714b62de11
5 changed files with 388 additions and 30 deletions

BIN
cmd/cmd

Binary file not shown.

View file

@ -4,6 +4,7 @@ import (
"eternity/common" "eternity/common"
"eternity/lib" "eternity/lib"
"fmt" "fmt"
"math/big"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
@ -55,11 +56,13 @@ func main() {
os.Exit(1) os.Exit(1)
} }
}() }()
tempDir, err, vagueError = lib.BuildEPK("./", inMemory, config.Build, logger) var size big.Int
size, tempDir, err, vagueError = lib.BuildEPK("./", inMemory, config.Build, logger)
if err != nil || vagueError != nil { if err != nil || vagueError != nil {
common.BuildEPKHandleError(tempDir, err, vagueError, logger) common.BuildEPKHandleError(tempDir, err, vagueError, logger)
} else { } else {
filePath := filepath.Join("./", config.Metadata.Name+"-"+config.Metadata.Version.String()) filePath := filepath.Join("./", config.Metadata.Name+"-"+config.Metadata.Version.String())
config.Metadata.DecompressedSize = size
err, vagueError := lib.PackageEPK(config.Metadata, config.Build, tempDir, filePath+".epk", privateKey, logger) err, vagueError := lib.PackageEPK(config.Metadata, config.Build, tempDir, filePath+".epk", privateKey, logger)
if err != nil || vagueError != nil { if err != nil || vagueError != nil {
common.PackageEPKHandleError(err, vagueError, logger) common.PackageEPKHandleError(err, vagueError, logger)
@ -72,6 +75,23 @@ func main() {
} }
} }
} }
case "repo":
if len(os.Args) < 4 {
fmt.Println("Usage: eternity repo <build/generate> <directory>")
fmt.Println("See 'eternity help repo' for more information.")
os.Exit(1)
}
privateKey, err, vagueError := common.LoadPrivateKey(logger)
if err != nil || vagueError != nil {
common.LoadPrivateKeyHandleError(err, vagueError, logger)
}
switch os.Args[2] {
case "generate":
err, vagueError := lib.GenerateRepository(os.Args[3], privateKey, logger)
if err != nil || vagueError != nil {
common.GenerateRepositoryHandleError(err, vagueError, logger)
}
}
case "help": case "help":
if len(os.Args) > 2 { if len(os.Args) > 2 {
switch os.Args[2] { switch os.Args[2] {

View file

@ -1,6 +1,7 @@
package common package common
import ( import (
"bufio"
"crypto/ed25519" "crypto/ed25519"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
@ -35,13 +36,13 @@ var DefaultLogger = lib.Logger{
fmt.Println(severityPretty, log.Content) fmt.Println(severityPretty, log.Content)
if log.Prompt { if log.Prompt {
fmt.Print(": ") fmt.Print(": ")
var userInput string reader := bufio.NewReader(os.Stdin)
_, err := fmt.Scanln(&userInput) userInput, err := reader.ReadString('\n')
if err != nil { if err != nil {
fmt.Println(color.RedString("[FATAL]"), "Failed to read user input:", err) fmt.Println(color.RedString("[FATAL]"), "Failed to read user input:", err)
os.Exit(1) os.Exit(1)
} else { } else {
return userInput return userInput[:len(userInput)-1]
} }
} }
return "" return ""
@ -154,6 +155,11 @@ func BuildEPKHandleError(tempDir string, err error, vagueError error, logger *li
Level: "FATAL", Level: "FATAL",
Content: "Failed to count files in target root: " + err.Error(), Content: "Failed to count files in target root: " + err.Error(),
}) })
case errors.Is(vagueError, lib.ErrBuildEPKBadBuildType):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Invalid build type: " + err.Error(),
})
} }
} }
@ -382,3 +388,50 @@ func LoadPrivateKeyHandleError(err error, vagueError error, logger *lib.Logger)
}) })
} }
} }
func GenerateRepositoryHandleError(err error, vagueError error, logger *lib.Logger) {
switch {
case errors.Is(vagueError, lib.ErrGenerateRepositoryStatError):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Failed to stat directory: " + err.Error(),
Prompt: false,
})
case errors.Is(vagueError, lib.ErrGenerateRepositoryNotDirectory):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Specified path is not a directory",
Prompt: false,
})
case errors.Is(vagueError, lib.ErrGenerateRepositoryFailedToWalk):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Failed to walk directory: " + err.Error(),
Prompt: false,
})
case errors.Is(vagueError, lib.ErrGenerateRepositoryCannotMarshalJSON):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Failed to marshal JSON: " + err.Error(),
Prompt: false,
})
case errors.Is(vagueError, lib.ErrGenerateRepositoryCannotOpenFile):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Failed to open file for writing: " + err.Error(),
Prompt: false,
})
case errors.Is(vagueError, lib.ErrGenerateRepositoryCannotWriteFile):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Failed to write to file: " + err.Error(),
Prompt: false,
})
case errors.Is(vagueError, lib.ErrGenerateRepositoryCannotCloseFile):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Failed to close file: " + err.Error(),
Prompt: false,
})
}
}

View file

@ -8,4 +8,3 @@
</magic> </magic>
</mime-type> </mime-type>
</mime-info> </mime-info>

View file

@ -8,6 +8,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"io" "io"
"math/big"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -34,8 +35,10 @@ type Metadata struct {
Author string Author string
License string License string
Architecture string Architecture string
Dependencies []string // The decompressed size may be larger than the int64 allocated for a compressed file
SpecialFiles SpecialFiles DecompressedSize big.Int
Dependencies []string
SpecialFiles SpecialFiles
} }
// Build is a struct that contains the build configuration of the package // Build is a struct that contains the build configuration of the package
@ -256,16 +259,17 @@ var ErrBuildEPKWritingBuildShError = errors.New("error writing to build.sh")
var ErrBuildEPKTargetRootError = errors.New("error creating target root") var ErrBuildEPKTargetRootError = errors.New("error creating target root")
var ErrBuildEPKExecutingBuildShError = errors.New("error executing build.sh") var ErrBuildEPKExecutingBuildShError = errors.New("error executing build.sh")
var ErrBuildEPKCountingFilesError = errors.New("error counting files") var ErrBuildEPKCountingFilesError = errors.New("error counting files")
var ErrBuildEPKBadBuildType = errors.New("bad build type")
// BuildEPK builds the EPK package into a build directory // BuildEPK builds the EPK package into a build directory
func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logger) (string, error, error) { func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logger) (big.Int, string, error, error) {
var tempDir string var tempDir string
switch buildConfig.Type { switch buildConfig.Type {
case "chroot": case "chroot":
return "", nil, ErrBuildEPKChrootError return *big.NewInt(0), "", nil, ErrBuildEPKChrootError
case "unrestricted": case "unrestricted":
return "", nil, ErrBuildEPKUnrestrictedError return *big.NewInt(0), "", nil, ErrBuildEPKUnrestrictedError
case "host": case "host":
// Set up the temp dir // Set up the temp dir
var err error var err error
@ -279,7 +283,7 @@ func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logge
tempDir, err = os.MkdirTemp(projectDir, "eternity-build-") tempDir, err = os.MkdirTemp(projectDir, "eternity-build-")
} }
if err != nil { if err != nil {
return tempDir, err, ErrBuildEPKTemporaryDirectoryError return *big.NewInt(0), tempDir, err, ErrBuildEPKTemporaryDirectoryError
} }
// Copy the hooks folder // Copy the hooks folder
@ -292,12 +296,12 @@ func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logge
err = os.MkdirAll(targetHooksDir, 0755) err = os.MkdirAll(targetHooksDir, 0755)
if err != nil { if err != nil {
return tempDir, err, ErrBuildEPKCreateHooksError return *big.NewInt(0), tempDir, err, ErrBuildEPKCreateHooksError
} }
err = os.CopyFS(targetHooksDir, os.DirFS(hooksDir)) err = os.CopyFS(targetHooksDir, os.DirFS(hooksDir))
if err != nil { if err != nil {
return tempDir, err, ErrBuildEPKCopyHooksError return *big.NewInt(0), tempDir, err, ErrBuildEPKCopyHooksError
} }
} }
@ -314,19 +318,19 @@ func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logge
file, err := os.OpenFile(tempDir+"/build.sh", os.O_CREATE|os.O_RDWR, 0755) file, err := os.OpenFile(tempDir+"/build.sh", os.O_CREATE|os.O_RDWR, 0755)
if err != nil { if err != nil {
return tempDir, err, ErrBuildEPKBuildShError return *big.NewInt(0), tempDir, err, ErrBuildEPKBuildShError
} }
_, err = file.WriteString(shellScript) _, err = file.WriteString(shellScript)
if err != nil { if err != nil {
return tempDir, err, ErrBuildEPKWritingBuildShError return *big.NewInt(0), tempDir, err, ErrBuildEPKWritingBuildShError
} }
// Set up the target root // Set up the target root
targetRoot := filepath.Join(tempDir, buildConfig.TargetRoot) targetRoot := filepath.Join(tempDir, buildConfig.TargetRoot)
err = os.MkdirAll(targetRoot, 0755) err = os.MkdirAll(targetRoot, 0755)
if err != nil { if err != nil {
return tempDir, err, ErrBuildEPKTargetRootError return *big.NewInt(0), tempDir, err, ErrBuildEPKTargetRootError
} }
// Execute the shell script in BWrap // Execute the shell script in BWrap
@ -369,11 +373,12 @@ func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logge
err = exec.Command("bwrap", arguments...).Run() err = exec.Command("bwrap", arguments...).Run()
if err != nil { if err != nil {
return tempDir, err, ErrBuildEPKExecutingBuildShError return *big.NewInt(0), tempDir, err, ErrBuildEPKExecutingBuildShError
} }
// Hopefully, the build was successful. Let's give the user a file count. // Hopefully, the build was successful. Let's give the user a file and size count.
var fileCount int var fileCount int
var sizeCount big.Int
// We start at -1 because the root directory is not counted // We start at -1 because the root directory is not counted
dirCount := -1 dirCount := -1
err = filepath.Walk(targetRoot, func(path string, info os.FileInfo, err error) error { err = filepath.Walk(targetRoot, func(path string, info os.FileInfo, err error) error {
@ -382,18 +387,25 @@ func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logge
} else { } else {
fileCount++ fileCount++
} }
// Both directories and files need to have their sizes counted
sizeCount.Add(&sizeCount, big.NewInt(info.Size()))
return nil return nil
}) })
if err != nil { if err != nil {
return tempDir, err, ErrBuildEPKCountingFilesError return *big.NewInt(0), tempDir, err, ErrBuildEPKCountingFilesError
} }
logger.LogFunc(Log{ logger.LogFunc(Log{
Level: "INFO", Content: "Build successful. " + strconv.Itoa(fileCount) + " files and " + strconv.Itoa(dirCount) + " directories created.", Prompt: false, Level: "INFO",
Content: "Build successful. " + strconv.Itoa(fileCount) + " files and " + strconv.Itoa(dirCount) +
" directories created," + " totalling " + sizeCount.String() + " bytes.",
Prompt: false,
}) })
}
return tempDir, nil, nil return sizeCount, tempDir, nil, nil
default:
return *big.NewInt(0), "", errors.New(buildConfig.Type), ErrBuildEPKBadBuildType
}
} }
// CreateTar creates a tar archive from a directory // CreateTar creates a tar archive from a directory
@ -463,13 +475,13 @@ var ConstPackageEPKBigEndian = []byte{0x62}
var ConstPackageEPKLittleEndian = []byte{0x6C} var ConstPackageEPKLittleEndian = []byte{0x6C}
// ConstPackageEPKInitialByteOffset is the initial byte offset for an EPK file until we arrive at the signature. 12 = 3 + 1 + 8: 3 for the magic number, 1 for the endian, and 8 for the tar offset // ConstPackageEPKInitialByteOffset is the initial byte offset for an EPK file until we arrive at the signature. 12 = 3 + 1 + 8: 3 for the magic number, 1 for the endian, and 8 for the tar offset
var ConstPackageEPKInitialByteOffset = 12 var ConstPackageEPKInitialByteOffset int64 = 12
// SignatureLength is the length of the signature // ConstPackageEPKSignatureLength is the length of the signature
var SignatureLength = 64 var ConstPackageEPKSignatureLength int64 = 64
// PublicKeyLength is the length of the public key // ConstPackageEPKPublicKeyLength is the length of the public key
var PublicKeyLength = 32 var ConstPackageEPKPublicKeyLength int64 = 32
// ConstPackageEPKMetadataOffset is the offset of the metadata in the EPK file // ConstPackageEPKMetadataOffset is the offset of the metadata in the EPK file
var ConstPackageEPKMetadataOffset = 108 var ConstPackageEPKMetadataOffset = 108
@ -536,6 +548,7 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
"noDelete": metaData.SpecialFiles.NoDelete, "noDelete": metaData.SpecialFiles.NoDelete,
"noReplace": metaData.SpecialFiles.NoReplace, "noReplace": metaData.SpecialFiles.NoReplace,
}, },
"size": metaData.DecompressedSize.String(),
} }
// Make the data template into a JSON string // Make the data template into a JSON string
@ -690,13 +703,13 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
} }
// Write the signature // Write the signature
_, err = file.WriteAt(signature, int64(ConstPackageEPKInitialByteOffset)) _, err = file.WriteAt(signature, ConstPackageEPKInitialByteOffset)
if err != nil { if err != nil {
return err, ErrPackageEPKCannotWriteFile return err, ErrPackageEPKCannotWriteFile
} }
// Write the public key // Write the public key
_, err = file.WriteAt(publicKey, int64(ConstPackageEPKInitialByteOffset)+int64(SignatureLength)) _, err = file.WriteAt(publicKey, ConstPackageEPKInitialByteOffset+ConstPackageEPKSignatureLength)
if err != nil { if err != nil {
return err, ErrPackageEPKCannotWriteFile return err, ErrPackageEPKCannotWriteFile
} }
@ -709,3 +722,276 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
return nil, nil return nil, nil
} }
// ConstGenerateRepositoryRepoDataOffset is the offset of the repository data in the repository.json file: it is 3 (magic) + 64 (the signature) + 32 (the public key) = 99
var ConstGenerateRepositoryRepoDataOffset int64 = 99
// ConstGenerateRepositoryEPKMagicNumber is the magic number for an EPK repository: "eon" in ASCII / UTF-8, for obvious reasons
var ConstGenerateRepositoryEPKMagicNumber = []byte{0x65, 0x6F, 0x6E}
var ErrGenerateRepositoryStatError = errors.New("error stating file or directory")
var ErrGenerateRepositoryNotDirectory = errors.New("not a directory")
var ErrGenerateRepositoryRepositoryNameContainsSlash = errors.New("repository name contains a slash")
var ErrGenerateRepositoryFailedToWalk = errors.New("error walking directory")
var ErrGenerateRepositoryCannotUnmarshalJSON = errors.New("error unmarshalling JSON")
var ErrGenerateRepositoryCannotMarshalJSON = errors.New("error marshalling JSON")
var ErrGenerateRepositoryCannotOpenFile = errors.New("error opening file for writing")
var ErrGenerateRepositoryCannotWriteFile = errors.New("error writing to file")
var ErrGenerateRepositoryCannotCloseFile = errors.New("error closing file")
func GenerateRepository(directory string, privateKey ed25519.PrivateKey, logger *Logger) (error, error) {
// First, we need to see if the directory exists
logger.LogFunc(Log{
Level: "INFO", Content: "Generating repository", Prompt: false,
})
info, err := os.Stat(directory)
if err != nil {
return err, ErrGenerateRepositoryStatError
}
if !info.IsDir() {
return nil, ErrGenerateRepositoryNotDirectory
}
// Create the EPK map
epkMap := make(map[string]interface{})
// See if the repository.json file exists
_, err = os.Stat(directory + "/repository.erf")
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return err, ErrGenerateRepositoryStatError
} else {
if logger.PromptSupported {
// Ask the user for the name of the repository
repoName := logger.LogFunc(Log{
Level: "PROMPT", Content: "Enter the name of the repository", Prompt: true,
})
// Check the repository name does not contain any slashes
if strings.Contains(repoName, "/") {
return nil, ErrGenerateRepositoryRepositoryNameContainsSlash
}
// Ask the user for the description of the repository
repoDesc := logger.LogFunc(Log{
Level: "PROMPT", Content: "Enter a short description of the repository", Prompt: true,
})
// Ask the user for the author of the repository
repoAuthor := logger.LogFunc(Log{
Level: "PROMPT",
Content: "Enter your preferred author name. This must be the same as the author name used in " +
"eternity.json and associated with your keypair, otherwise it will cause issues with EPK" +
" verification and your repository will be rejected by Eon and cannot be trusted.",
Prompt: true,
})
// Now append the metadata to the EPK map
epkMap["name"] = repoName
epkMap["desc"] = repoDesc
epkMap["author"] = repoAuthor
} else {
logger.LogFunc(Log{
Level: "FATAL",
Content: "Please fill in the author, name, and description of the repository in repository.json. " +
"Your author name must be the same as the author name used in eternity.json and associated with " +
"your keypair, otherwise it will cause issues with EPK verification and your repository will be " +
"rejected by Eon and cannot be trusted.",
Prompt: false,
})
}
}
} else {
// Since it does exist, we can extract the name and description from it
file, err := os.ReadFile(directory + "/repository.erf")
if err != nil {
return err, ErrGenerateRepositoryCannotOpenFile
}
// Unmarshal the JSON
var oldRepositoryMap map[string]interface{}
err = json.Unmarshal(file[ConstGenerateRepositoryRepoDataOffset:], &oldRepositoryMap)
if err != nil {
return err, ErrGenerateRepositoryCannotUnmarshalJSON
}
// Copy the author, name, and description to the EPK map
epkMap["name"] = oldRepositoryMap["name"]
epkMap["desc"] = oldRepositoryMap["desc"]
epkMap["author"] = oldRepositoryMap["author"]
}
// Add a list of packages to the EPK map
epkMap["packages"] = make([]map[string]interface{}, 0)
// Now, walk the directory
err = filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
// If error is not nil, return it
if err != nil {
return err
}
// Ignore directories
if info.IsDir() {
return nil
}
// Ok. We need to check if the file actually is an EPK file
file, err := os.Open(path)
if err != nil {
return err
}
// Read the first 3 bytes
magicNumber := make([]byte, 3)
_, err = file.Read(magicNumber)
if err != nil {
return err
}
// Check if the magic number is correct
if !bytes.Equal(magicNumber, ConstPackageEPKMagicNumber) {
// It isn't an EPK file, so we can ignore it
return nil
}
// We need to create a hash of the file
xxHash := xxhash.New()
_, err = io.Copy(xxHash, file)
if err != nil {
return err
}
// Extract the metadata. First, we get the endian-ness
var littleEndian bool
endian := make([]byte, 1)
_, err = file.ReadAt(endian, 3)
if err != nil {
return err
}
if bytes.Equal(endian, ConstPackageEPKLittleEndian) {
littleEndian = true
} else if bytes.Equal(endian, ConstPackageEPKBigEndian) {
littleEndian = false
} else {
return errors.New("invalid endianness")
}
// Now we get the tar offset
var tarOffset int64
tarOffsetBytes := make([]byte, 8)
_, err = file.ReadAt(tarOffsetBytes, 4)
if err != nil {
return err
}
// Now we convert the tar offset to an int64
if littleEndian {
tarOffset = int64(binary.LittleEndian.Uint64(tarOffsetBytes))
} else {
tarOffset = int64(binary.BigEndian.Uint64(tarOffsetBytes))
}
// Now we can read in the metadata
metadataBytes := make([]byte, tarOffset-int64(ConstPackageEPKMetadataOffset))
_, err = file.ReadAt(metadataBytes, int64(ConstPackageEPKMetadataOffset))
if err != nil {
return err
}
// Now we can unmarshal the metadata
var metadata map[string]interface{}
err = json.Unmarshal(metadataBytes, &metadata)
if err != nil {
return err
}
// Now we have the hash, we need to add it to our data template
dataTemplate := make(map[string]interface{})
dataTemplate["hash"] = xxHash.Sum64()
// Now we add some basic metadata
dataTemplate["name"] = metadata["name"]
dataTemplate["author"] = metadata["author"]
dataTemplate["version"] = metadata["version"]
dataTemplate["size"] = metadata["size"]
dataTemplate["arch"] = metadata["arch"]
dataTemplate["desc"] = metadata["desc"]
dataTemplate["deps"] = metadata["deps"]
// We add the path to the EPK file, relative to the directory
relativePath, err := filepath.Rel(directory, path)
if err != nil {
return err
}
dataTemplate["path"] = relativePath
// Append it to a list in the EPK map
epkMap["packages"] = append(epkMap["packages"].([]map[string]interface{}), dataTemplate)
return nil
})
// This error message is a bit vague, but meh.
if err != nil {
return err, ErrGenerateRepositoryFailedToWalk
}
// Great, now we need to marshal the EPK map and write it to a file
epkMapBytes, err := json.Marshal(epkMap)
if err != nil {
return err, ErrGenerateRepositoryCannotMarshalJSON
}
// Write the EPK map to a file
file, err := os.OpenFile(directory+"/repository.erf", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
if err != nil {
return err, ErrGenerateRepositoryCannotOpenFile
}
// Sign the epk map
xxHash := xxhash.New()
_, err = xxHash.Write(epkMapBytes)
if err != nil {
return err, nil
}
signature := ed25519.Sign(privateKey, xxHash.Sum(nil))
publicKey := privateKey.Public().(ed25519.PublicKey)
// Write magic number
_, err = file.Write(ConstGenerateRepositoryEPKMagicNumber)
if err != nil {
return err, ErrGenerateRepositoryCannotWriteFile
}
// Write signature
_, err = file.WriteAt(signature, 3)
if err != nil {
return err, ErrGenerateRepositoryCannotWriteFile
}
// Write public key
_, err = file.WriteAt(publicKey, 67)
if err != nil {
return err, ErrGenerateRepositoryCannotWriteFile
}
// Write the EPK map to the file
_, err = file.WriteAt(epkMapBytes, ConstGenerateRepositoryRepoDataOffset)
if err != nil {
return err, ErrGenerateRepositoryCannotWriteFile
}
// Close the file
err = file.Close()
if err != nil {
return err, ErrGenerateRepositoryCannotCloseFile
}
return nil, nil
}