Compare commits

...

10 commits

10 changed files with 934 additions and 243 deletions

3
.gitignore vendored
View file

@ -1 +1,4 @@
.idea
.vscode
/bin
*.epk

View file

@ -3,3 +3,34 @@
Eternity is the build tool for eon: It builds packages at speeds RPMBUILD wishes it could.
Currently not suitable for production use.
## Dependencies
To use eternity, you must install:
go 1.23^
fakeroot-tcp 1.36^
## Quick Start
To simply build and install on your machine, run:
```
./build.sh
```
## Building
To build the cli, run:
```
mkdir bin
CGO_ENABLED=0 go build -ldflags "-s -w -v" -o ./bin/eternity ./cmd/main.go
```
## Installation
To install your eternity CLI after building it, run:
```
sudo install ./bin/eternity /bin
```

13
build.sh Executable file
View file

@ -0,0 +1,13 @@
#!/bin/sh
# Create the bin directory
mkdir -p bin
# Build the CLI
echo "Building the CLI..."
CGO_ENABLED=0 go build -ldflags "-s -w" -o ./bin/eternity ./cmd/main.go
# Install the CLI
echo "Installing the CLI..."
sudo install ./bin/eternity /bin
echo "CLI installed successfully!"

BIN
cmd/cmd

Binary file not shown.

View file

@ -1,12 +1,19 @@
package main
import (
"eternity/common"
"eternity/lib"
"fmt"
"os"
"regexp"
"strings"
"git.oreonproject.org/oreonproject/eternity/common"
"git.oreonproject.org/oreonproject/eternity/lib"
"os/signal"
"path/filepath"
"github.com/Masterminds/semver"
"github.com/sassoftware/go-rpmutils"
)
func main() {
@ -34,6 +41,31 @@ func main() {
}
}
// Allow the user to choose an output dir
var outputDir = "./" // Default output directory
// Loop through arguments to find "-o" or "--output"
for i := 2; i < len(os.Args); i++ {
if os.Args[i] == "-o" || os.Args[i] == "--output" {
// Check if next argument exists and is not another flag
if i+1 < len(os.Args) && !strings.HasPrefix(os.Args[i+1], "-") {
outputDir = os.Args[i+1]
if !filepath.IsAbs(outputDir) {
outputDir, _ = filepath.Abs(outputDir) // Convert to absolute path if relative
}
break
} else {
fmt.Println("FATAL: Output directory not specified or invalid after", os.Args[i])
os.Exit(1)
}
}
}
logger.LogFunc(lib.Log{
Level: "INFO",
Content: "Build directory set to: " + outputDir,
})
privateKey, err, vagueError := common.LoadPrivateKey(logger)
if err != nil || vagueError != nil {
common.LoadPrivateKeyHandleError(err, vagueError, logger)
@ -55,11 +87,13 @@ func main() {
os.Exit(1)
}
}()
tempDir, err, vagueError = lib.BuildEPK("./", inMemory, config.Build, logger)
var size int64
size, tempDir, err, vagueError = lib.BuildEPK("./", inMemory, config.Build, logger)
if err != nil || vagueError != nil {
common.BuildEPKHandleError(tempDir, err, vagueError, logger)
} else {
filePath := filepath.Join("./", config.Metadata.Name+"-"+config.Metadata.Version.String())
filePath := filepath.Join(outputDir, config.Metadata.Name+"-"+config.Metadata.Version.String())
config.Metadata.DecompressedSize = size
err, vagueError := lib.PackageEPK(config.Metadata, config.Build, tempDir, filePath+".epk", privateKey, logger)
if err != nil || vagueError != nil {
common.PackageEPKHandleError(err, vagueError, logger)
@ -72,6 +106,201 @@ 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 "convert":
if len(os.Args) < 4 {
fmt.Println("Usage: eternity convert </path/to/rpm> [</path/to/output/epk>]")
os.Exit(1)
}
privateKey, err, vagueError := common.LoadPrivateKey(logger)
if err != nil || vagueError != nil {
common.LoadPrivateKeyHandleError(err, vagueError, logger)
}
rpmFile, err := os.Open(os.Args[2])
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error opening RPM: " + err.Error(),
})
}
rpm, err := rpmutils.ReadRpm(rpmFile)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error reading RPM: " + err.Error(),
})
}
name, err := rpm.Header.GetString(rpmutils.NAME)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM name: " + err.Error(),
})
}
description, err := rpm.Header.GetString(rpmutils.DESCRIPTION)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM description: " + err.Error(),
})
}
longDescription := logger.LogFunc(lib.Log{
Level: "INFO",
Content: "Enter a long description for the package: ",
Prompt: true,
})
version, err := rpm.Header.GetString(rpmutils.VERSION)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM version: " + err.Error(),
})
}
release, err := rpm.Header.GetString(rpmutils.RELEASE)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM release: " + err.Error(),
})
}
semanticVersion, err := semver.NewVersion(version + "-" + release)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error parsing RPM version and release: " + err.Error(),
})
}
author := logger.LogFunc(lib.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,
})
license, err := rpm.Header.GetString(rpmutils.LICENSE)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM license: " + err.Error(),
})
}
decompressedSize, err := rpm.Header.GetUint64s(rpmutils.SIZE)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM size: " + err.Error(),
})
}
dependenciesRpm, err := rpm.Header.GetStrings(rpmutils.REQUIRENAME)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM dependencies: " + err.Error(),
})
}
libRegex := regexp.MustCompile(`^([a-z]+)`)
var dependencies []string
for _, dependency := range dependenciesRpm {
dependencyMatch := libRegex.FindStringSubmatch(dependency)
if len(dependencyMatch) > 1 {
match := dependencyMatch[1]
if match != "rpmlib" && match != "rtld" {
var exists bool
for _, dep := range dependencies {
if dep == match {
exists = true
}
}
if !exists {
dependencies = append(dependencies, match)
}
}
}
}
fmt.Println(dependencies)
architecture, err := rpm.Header.GetString(rpmutils.ARCH)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM architecture: " + err.Error(),
})
}
metadata := lib.Metadata{
Name: name,
Description: description,
LongDescription: longDescription,
Version: semanticVersion,
Author: author,
License: license,
Architecture: architecture,
Dependencies: dependencies,
DecompressedSize: int64(decompressedSize[0]),
SpecialFiles: lib.SpecialFiles{},
}
// Output the RPM data to a temporary directory
tempDir, err := os.MkdirTemp("", "eternity-convert-")
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error creating temporary directory: " + err.Error(),
})
}
err = rpm.ExpandPayload(filepath.Join(tempDir, "payload"))
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error expanding RPM payload: " + err.Error(),
})
}
// Package the EPK
err, vagueError = lib.PackageEPK(metadata, lib.Build{
TargetRoot: "payload",
}, tempDir, os.Args[3], privateKey, logger)
if err != nil || vagueError != nil {
common.PackageEPKHandleError(err, vagueError, logger)
} else {
logger.LogFunc(lib.Log{
Level: "INFO",
Content: "Successfully converted RPM to EPK at " + os.Args[3],
})
}
case "help":
if len(os.Args) > 2 {
switch os.Args[2] {
@ -79,7 +308,7 @@ func main() {
fmt.Println("Usage: eternity build [-d/--disk]")
fmt.Println(" -d, --disk Build EPK on disk instead of in memory. This is useful for large EPKs.")
case "convert":
fmt.Println("Usage: eternity convert </path/to/rpm> [</path/to/output/epk>]")
fmt.Println("Usage: eternity convert </path/to/rpm> </path/to/output/epk>")
case "repo":
fmt.Println("Usage: eternity repo <build/generate> <directory>")
fmt.Println(" build Build a repository from a directory containing EPK project directories.")

View file

@ -1,15 +1,18 @@
package common
import (
"bufio"
"errors"
"fmt"
"os"
"strings"
"git.oreonproject.org/oreonproject/eternity/lib"
"crypto/ed25519"
"crypto/x509"
"encoding/pem"
"errors"
"eternity/lib"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/fatih/color"
)
@ -35,18 +38,20 @@ var DefaultLogger = lib.Logger{
fmt.Println(severityPretty, log.Content)
if log.Prompt {
fmt.Print(": ")
var userInput string
_, err := fmt.Scanln(&userInput)
reader := bufio.NewReader(os.Stdin)
userInput, err := reader.ReadString('\n')
if err != nil {
fmt.Println(color.RedString("[FATAL]"), "Failed to read user input:", err)
os.Exit(1)
} else {
return userInput
return userInput[:len(userInput)-1]
}
}
return ""
},
PromptSupported: true,
StdoutSupported: true,
Stdout: os.Stdout,
}
// EternityJsonHandleError handles errors related to the parsing of eternity.json
@ -154,6 +159,11 @@ func BuildEPKHandleError(tempDir string, err error, vagueError error, logger *li
Level: "FATAL",
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 +392,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>
</mime-type>
</mime-info>

11
go.mod
View file

@ -1,4 +1,4 @@
module eternity
module git.oreonproject.org/oreonproject/eternity
go 1.23
@ -7,10 +7,17 @@ require (
github.com/cespare/xxhash/v2 v2.3.0
github.com/fatih/color v1.17.0
github.com/klauspost/compress v1.17.9
github.com/sassoftware/go-rpmutils v0.4.0
)
require (
github.com/DataDog/zstd v1.5.6 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/cloudflare/circl v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.24.0 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/sys v0.25.0 // indirect
)

76
go.sum
View file

@ -1,7 +1,23 @@
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY=
github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI=
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
@ -11,7 +27,67 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=
github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -1,19 +1,20 @@
package lib
import (
"archive/tar"
"bytes"
"crypto/ed25519"
"encoding/binary"
"encoding/json"
"errors"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"archive/tar"
"crypto/ed25519"
"encoding/binary"
"encoding/json"
"os/exec"
"path/filepath"
"github.com/Masterminds/semver"
"github.com/cespare/xxhash/v2"
"github.com/klauspost/compress/zstd"
@ -21,43 +22,40 @@ import (
// SpecialFiles is a struct that contains the special files that are not to be deleted or replaced
type SpecialFiles struct {
NoDelete []string
NoReplace []string
NoDelete []string `json:"noDelete"`
NoReplace []string `json:"noReplace"`
}
// Metadata is a struct that contains the metadata of the package
type Metadata struct {
Name string
Description string
LongDescription string
Version semver.Version
Author string
License string
Architecture string
Dependencies []string
SpecialFiles SpecialFiles
Name string `json:"name"`
Description string `json:"desc"`
LongDescription string `json:"longDesc"`
Version *semver.Version
VersionString string `json:"version"`
Author string `json:"author"`
License string `json:"license"`
Architecture string `json:"arch"`
// The decompressed size may be larger than the int64 allocated for a compressed file
DecompressedSize int64
Dependencies []string `json:"deps"`
SpecialFiles SpecialFiles `json:"specialFiles"`
}
// Build is a struct that contains the build configuration of the package
type Build struct {
Type string
Dependencies []string
Steps []string
TargetRoot string
HooksFolder PotentiallyNullString
FilesFolder PotentiallyNullString
}
// PotentiallyNullString is a struct that contains a string that may be null
type PotentiallyNullString struct {
Value string
Null bool
Type string `json:"type"`
Dependencies []string `json:"deps"`
Steps []string `json:"steps"`
TargetRoot string `json:"root"`
HooksFolder string `json:"hooks"`
FilesFolder string `json:"files"`
}
// Config is a struct that contains the configuration of the package
type Config struct {
Metadata Metadata
Build Build
Metadata Metadata `json:"metadata"`
Build Build `json:"build"`
}
// Log is a struct that contains the log information
@ -71,6 +69,8 @@ type Log struct {
type Logger struct {
LogFunc func(Log) string
PromptSupported bool
StdoutSupported bool
Stdout io.Writer
}
var ErrEternityJsonOpenError = errors.New("error opening eternity.json")
@ -78,22 +78,6 @@ var ErrEternityJsonReadError = errors.New("error reading eternity.json")
var ErrEternityJsonParseError = errors.New("error parsing eternity.json")
var ErrEternityJsonMapError = errors.New("error mapping eternity.json")
// interfaceToStringSlice converts an interface slice to a string slice
func interfaceToStringSlice(interfaceSlice []interface{}, interfaceName string) ([]string, error) {
// Yes, it's meant to be empty and not nil: JSON arrays are empty, not nil
//goland:noinspection GoPreferNilSlice
stringSlice := []string{}
for _, interfaceValue := range interfaceSlice {
stringValue, ok := interfaceValue.(string)
if !ok {
return nil, errors.New(interfaceName + " are not strings")
}
stringSlice = append(stringSlice, stringValue)
}
return stringSlice, nil
}
// ParseConfig parses the eternity.json file
func ParseConfig(path string, logger *Logger) (Config, error, error) {
// Open eternity.json
@ -107,143 +91,22 @@ func ParseConfig(path string, logger *Logger) (Config, error, error) {
return Config{}, err, ErrEternityJsonOpenError
}
// Convert the file to a byte buffer
var fileBytes bytes.Buffer
_, err = io.Copy(&fileBytes, file)
if err != nil {
return Config{}, err, ErrEternityJsonReadError
}
// Parse the file as JSON
var config map[string]interface{}
err = json.Unmarshal(fileBytes.Bytes(), &config)
var config Config
decoder := json.NewDecoder(file)
err = decoder.Decode(&config)
if err != nil {
return Config{}, err, ErrEternityJsonParseError
}
// Map SpecialFiles
var parsedSpecialFiles SpecialFiles
specialFiles, ok := config["specialFiles"].(map[string]interface{})
if !ok {
return Config{}, errors.New("specialFiles is not an object"), ErrEternityJsonMapError
}
noDelete, ok := specialFiles["noDelete"].([]interface{})
if !ok {
return Config{}, errors.New("noDelete is not an array"), ErrEternityJsonMapError
}
parsedSpecialFiles.NoDelete, err = interfaceToStringSlice(noDelete, "noDelete")
// Map the JSON version to a semver version
config.Metadata.Version, err = semver.NewVersion(config.Metadata.VersionString)
if err != nil {
return Config{}, err, ErrEternityJsonMapError
}
noReplace, ok := specialFiles["noReplace"].([]interface{})
if !ok {
return Config{}, errors.New("noReplace is not an array"), ErrEternityJsonMapError
}
parsedSpecialFiles.NoReplace, err = interfaceToStringSlice(noReplace, "noReplace")
if err != nil {
return Config{}, err, ErrEternityJsonMapError
}
// Declare the parsedMetadata object
var parsedMetadata Metadata
// Append parsedSpecialFiles to parsedMetadata
parsedMetadata.SpecialFiles = parsedSpecialFiles
// Map the metadata
parsedMetadata.Name, ok = config["name"].(string)
if !ok {
return Config{}, errors.New("name is not a string"), ErrEternityJsonMapError
}
parsedMetadata.Description, ok = config["desc"].(string)
if !ok {
return Config{}, errors.New("description is not a string"), ErrEternityJsonMapError
}
parsedMetadata.LongDescription, ok = config["longDesc"].(string)
if !ok {
return Config{}, errors.New("longDesc is not a string"), ErrEternityJsonMapError
}
versionString, ok := config["version"].(string)
if !ok {
return Config{}, errors.New("version is not a string"), ErrEternityJsonMapError
}
versionPointer, err := semver.NewVersion(versionString)
if err != nil {
return Config{}, err, ErrEternityJsonMapError
}
parsedMetadata.Version = *versionPointer
parsedMetadata.Author, ok = config["author"].(string)
if !ok {
return Config{}, errors.New("author is not a string"), ErrEternityJsonMapError
}
parsedMetadata.License, ok = config["license"].(string)
if !ok {
return Config{}, errors.New("license is not a string"), ErrEternityJsonMapError
}
parsedMetadata.Architecture, ok = config["arch"].(string)
if !ok {
return Config{}, errors.New("arch is not a string"), ErrEternityJsonMapError
}
dependencies, ok := config["deps"].([]interface{})
if !ok {
return Config{}, errors.New("deps is not an array"), ErrEternityJsonMapError
}
parsedMetadata.Dependencies, err = interfaceToStringSlice(dependencies, "dependencies")
if err != nil {
return Config{}, err, ErrEternityJsonMapError
}
// Map build
var parsedBuild Build
build, ok := config["build"].(map[string]interface{})
if !ok {
return Config{}, errors.New("build is not an object"), ErrEternityJsonMapError
}
parsedBuild.Type, ok = build["type"].(string)
if !ok {
return Config{}, errors.New("type is not a string"), ErrEternityJsonMapError
}
buildDependencies, ok := build["deps"].([]interface{})
if !ok {
return Config{}, errors.New("deps is not an array"), ErrEternityJsonMapError
}
parsedBuild.Dependencies, err = interfaceToStringSlice(buildDependencies, "deps")
if err != nil {
return Config{}, err, ErrEternityJsonMapError
}
steps, ok := build["steps"].([]interface{})
if !ok {
return Config{}, errors.New("steps is not an array"), ErrEternityJsonMapError
}
parsedBuild.Steps, err = interfaceToStringSlice(steps, "steps")
if err != nil {
return Config{}, err, ErrEternityJsonMapError
}
parsedBuild.TargetRoot, ok = build["root"].(string)
if !ok {
return Config{}, errors.New("root is not a string"), ErrEternityJsonMapError
}
hooksFolder, ok := build["hooks"].(string)
if !ok {
parsedBuild.HooksFolder = PotentiallyNullString{Null: true}
} else {
parsedBuild.HooksFolder = PotentiallyNullString{Null: false, Value: hooksFolder}
}
filesFolder, ok := build["files"].(string)
if !ok {
parsedBuild.FilesFolder = PotentiallyNullString{Null: true}
} else {
parsedBuild.FilesFolder = PotentiallyNullString{Null: false, Value: filesFolder}
}
// Create the final Config object
parsedConfig := Config{
Metadata: parsedMetadata,
Build: parsedBuild,
}
// Return the final Config object
return parsedConfig, nil, nil
return config, nil, nil
}
var ErrBuildEPKTemporaryDirectoryError = errors.New("error creating temporary directory")
@ -256,48 +119,66 @@ var ErrBuildEPKWritingBuildShError = errors.New("error writing to build.sh")
var ErrBuildEPKTargetRootError = errors.New("error creating target root")
var ErrBuildEPKExecutingBuildShError = errors.New("error executing build.sh")
var ErrBuildEPKCountingFilesError = errors.New("error counting files")
var ErrBuildEPKBadBuildType = errors.New("bad build type")
var ErrBuildEPKDirectoryDoesNotExist = errors.New("required directory does not exist")
var ErrBubbleWrapInsufficientPermissions = errors.New("bwrap does not have the necessary permissions")
// 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) (int64, string, error, error) {
var tempDir string
switch buildConfig.Type {
case "chroot":
return "", nil, ErrBuildEPKChrootError
return 0, "", nil, ErrBuildEPKChrootError
case "unrestricted":
return "", nil, ErrBuildEPKUnrestrictedError
return 0, "", nil, ErrBuildEPKUnrestrictedError
case "host":
// Set up the temp dir
var err error
if inMemory {
// Builds in /tmp. This means that the program must fit in RAM. Luckily, most programs do.
// If you're building a large program, you might want to consider using a disk build.
logger.LogFunc(Log{
Level: "INFO",
Content: "Creating temp directory",
Prompt: false,
})
tempDir, err = os.MkdirTemp("/tmp", "eternity-build-")
} else {
// Builds on disk. This is slower but if your program can't fit in RAM, you're out of luck.
// If your program can fit in RAM, you might want to consider using an in-memory build.
logger.LogFunc(Log{
Level: "INFO",
Content: "Creating temp directory on disk",
Prompt: false,
})
tempDir, err = os.MkdirTemp(projectDir, "eternity-build-")
}
if err != nil {
return tempDir, err, ErrBuildEPKTemporaryDirectoryError
logger.LogFunc(Log{
Level: "INFO",
Content: "Failed to create temporary directory, returning the error",
Prompt: false,
})
return 0, tempDir, err, ErrBuildEPKTemporaryDirectoryError
}
// Copy the hooks folder
if buildConfig.HooksFolder.Null != true {
hooksDir := filepath.Join(projectDir, buildConfig.HooksFolder.Value)
targetHooksDir := filepath.Join(tempDir, buildConfig.HooksFolder.Value)
if buildConfig.HooksFolder != "" {
hooksDir := filepath.Join(projectDir, buildConfig.HooksFolder)
targetHooksDir := filepath.Join(tempDir, buildConfig.HooksFolder)
logger.LogFunc(Log{
Level: "INFO", Content: "Copying hooks from " + hooksDir + " to " + targetHooksDir, Prompt: false,
})
err = os.MkdirAll(targetHooksDir, 0755)
if err != nil {
return tempDir, err, ErrBuildEPKCreateHooksError
return 0, tempDir, err, ErrBuildEPKCreateHooksError
}
err = os.CopyFS(targetHooksDir, os.DirFS(hooksDir))
if err != nil {
return tempDir, err, ErrBuildEPKCopyHooksError
return 0, tempDir, err, ErrBuildEPKCopyHooksError
}
}
@ -314,19 +195,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)
if err != nil {
return tempDir, err, ErrBuildEPKBuildShError
return 0, tempDir, err, ErrBuildEPKBuildShError
}
_, err = file.WriteString(shellScript)
if err != nil {
return tempDir, err, ErrBuildEPKWritingBuildShError
return 0, tempDir, err, ErrBuildEPKWritingBuildShError
}
// Set up the target root
targetRoot := filepath.Join(tempDir, buildConfig.TargetRoot)
err = os.MkdirAll(targetRoot, 0755)
if err != nil {
return tempDir, err, ErrBuildEPKTargetRootError
return 0, tempDir, err, ErrBuildEPKTargetRootError
}
// Execute the shell script in BWrap
@ -338,6 +219,62 @@ func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logge
// copy-pasted most of the host files into the container, then disabled networking. This also allows us to use
// fakeroot and minimises the blast radius of a malicious package (hopefully) by not allowing the home directory
// or any files owned by root to be viewed or modified (too bad if you've got sensitive data in /var or /etc :P)
// Ensure all directories being bound exist
directoriesToCheck := []string{"/bin", "/lib", "/lib64", "/usr", "/etc", "/var", "/sys", "/opt", targetRoot, tempDir}
for _, dir := range directoriesToCheck {
if _, err := os.Stat(dir); os.IsNotExist(err) {
logger.LogFunc(Log{
Level: "ERROR",
Content: "Directory does not exist: " + dir,
Prompt: false,
})
return 0, tempDir, err, ErrBuildEPKDirectoryDoesNotExist
}
}
// ensure fakeroot-tcp is installed at /usr/bin/fakeroot-tcp
// if _, err := os.Stat("/usr/bin/fakeroot-tcp"); os.IsNotExist(err) {
// logger.LogFunc(Log{
// Level: "FATAL",
// Content: "fakeroot-tcp is not installed",
// Prompt: false,
// })
// return 0, tempDir, err, errors.New("fakeroot-tcp is not installed")
// }
// Check if bwrap is available and has the necessary permissions
logger.LogFunc(Log{
Level: "INFO",
Content: "Checking if bwrap is available and has the necessary permissions",
Prompt: false,
})
cmd := exec.Command("bwrap", "--version")
err = cmd.Run()
if err != nil {
logger.LogFunc(Log{
Level: "ERROR",
Content: "bwrap is not available or does not have the necessary permissions",
Prompt: false,
})
return 0, tempDir, err, ErrBubbleWrapInsufficientPermissions
}
logger.LogFunc(Log{
Level: "DEBUG",
Content: "Temp directory: " + tempDir,
Prompt: false,
})
logger.LogFunc(Log{
Level: "DEBUG",
Content: "Target root: " + targetRoot,
Prompt: false,
})
logger.LogFunc(Log{
Level: "DEBUG",
Content: "Joined path: " + filepath.Join("/", buildConfig.TargetRoot),
Prompt: false,
})
arguments := []string{
"--unshare-net",
"--bind", "/bin", "/bin",
@ -354,26 +291,75 @@ func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logge
"--tmpfs", "/run",
"--tmpfs", "/tmp",
"--proc", "/proc",
"/usr/bin/fakeroot-tcp", "--",
"/usr/bin/fakeroot-tcp", "--", // fakeroot-tcp still installs as "fakeroot" for some reason TODO: handle wrong fakeroot installation
"/bin/sh", "/eternity/build.sh",
}
if buildConfig.FilesFolder.Null != true {
if buildConfig.FilesFolder != "" {
logger.LogFunc(Log{
Level: "INFO",
Content: "Binding files folder to container",
Prompt: false,
})
arguments = arguments[:len(arguments)-4]
arguments = append(
arguments, "--bind", filepath.Join(projectDir, buildConfig.FilesFolder.Value), filepath.Join("/", buildConfig.FilesFolder.Value),
arguments, "--bind", filepath.Join(projectDir, buildConfig.FilesFolder), filepath.Join("/", buildConfig.FilesFolder),
"/usr/bin/fakeroot-tcp", "--",
"/bin/sh", "/eternity/build.sh",
)
}
err = exec.Command("bwrap", arguments...).Run()
logger.LogFunc(Log{
Level: "INFO",
Content: "Executing build script in container",
Prompt: false,
})
logger.LogFunc(Log{
Level: "DEBUG",
Content: "Command: bwrap " + strings.Join(arguments, " "),
Prompt: false,
})
cmd = exec.Command("bwrap", arguments...)
if logger.StdoutSupported {
cmd.Stdout = logger.Stdout
}
var stdoutBuf, stderrBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf
err = cmd.Run()
stdoutStr := stdoutBuf.String()
stderrStr := stderrBuf.String()
if logger.StdoutSupported {
logger.Stdout.Write([]byte(stdoutStr))
logger.Stdout.Write([]byte(stderrStr))
}
logger.LogFunc(Log{
Level: "INFO",
Content: "Command stdout: " + stdoutStr,
Prompt: false,
})
logger.LogFunc(Log{
Level: "INFO",
Content: "Command stderr: " + stderrStr,
Prompt: false,
})
if err != nil {
return tempDir, err, ErrBuildEPKExecutingBuildShError
logger.LogFunc(Log{
Level: "ERROR",
Content: "Error occurred while executing build.sh, returning error",
Prompt: false,
})
return 0, tempDir, err, ErrBuildEPKExecutingBuildShError
}
// Hopefully, the build was successful. Let's give the user a file count.
logger.LogFunc(Log{
Level: "INFO",
Content: "Build finished",
Prompt: false,
})
// Hopefully, the build was successful. Let's give the user a file and size count.
var fileCount int
var sizeCount int64
// We start at -1 because the root directory is not counted
dirCount := -1
err = filepath.Walk(targetRoot, func(path string, info os.FileInfo, err error) error {
@ -382,18 +368,25 @@ func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logge
} else {
fileCount++
}
// Both directories and files need to have their sizes counted
sizeCount += info.Size()
return nil
})
if err != nil {
return tempDir, err, ErrBuildEPKCountingFilesError
return 0, tempDir, err, ErrBuildEPKCountingFilesError
}
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 " + strconv.FormatInt(sizeCount, 10) + " bytes.",
Prompt: false,
})
}
return tempDir, nil, nil
return sizeCount, tempDir, nil, nil
default:
return 0, "", errors.New(buildConfig.Type), ErrBuildEPKBadBuildType
}
}
// CreateTar creates a tar archive from a directory
@ -404,7 +397,7 @@ func CreateTar(targetDir string, output io.Writer) error {
return err
}
if !info.Mode().IsRegular() {
if !info.Mode().IsRegular() && info.Mode()&os.ModeSymlink == 0 {
return nil
}
@ -415,11 +408,20 @@ func CreateTar(targetDir string, output io.Writer) error {
header.Name = strings.TrimPrefix(strings.Replace(path, targetDir, "", -1), string(filepath.Separator))
if info.Mode()&os.ModeSymlink != 0 {
linkTarget, err := os.Readlink(path)
if err != nil {
return err
}
header.Linkname = linkTarget
}
err = tarWriter.WriteHeader(header)
if err != nil {
return err
}
if info.Mode().IsRegular() {
file, err := os.Open(path)
if err != nil {
return err
@ -434,6 +436,7 @@ func CreateTar(targetDir string, output io.Writer) error {
if err != nil {
return err
}
}
return nil
})
@ -463,13 +466,13 @@ var ConstPackageEPKBigEndian = []byte{0x62}
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
var ConstPackageEPKInitialByteOffset = 12
var ConstPackageEPKInitialByteOffset int64 = 12
// SignatureLength is the length of the signature
var SignatureLength = 64
// ConstPackageEPKSignatureLength is the length of the signature
var ConstPackageEPKSignatureLength int64 = 64
// PublicKeyLength is the length of the public key
var PublicKeyLength = 32
// ConstPackageEPKPublicKeyLength is the length of the public key
var ConstPackageEPKPublicKeyLength int64 = 32
// ConstPackageEPKMetadataOffset is the offset of the metadata in the EPK file
var ConstPackageEPKMetadataOffset = 108
@ -510,8 +513,8 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
return err, ErrPackageEPKMoveToDistError
}
if build.HooksFolder.Null != true {
hooksDir := filepath.Join(tempDir, build.HooksFolder.Value)
if build.HooksFolder != "" {
hooksDir := filepath.Join(tempDir, build.HooksFolder)
err = os.Rename(hooksDir, distDir+"/hooks")
if err != nil {
return err, ErrPackageEPKMoveToDistError
@ -536,6 +539,7 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
"noDelete": metaData.SpecialFiles.NoDelete,
"noReplace": metaData.SpecialFiles.NoReplace,
},
"size": metaData.DecompressedSize,
}
// Make the data template into a JSON string
@ -556,8 +560,7 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
}
// Calculate the tar offset
var tarOffset int64
tarOffset = int64(ConstPackageEPKMetadataOffset) + dataTemplateLength
var tarOffset int64 = int64(ConstPackageEPKMetadataOffset) + dataTemplateLength
logger.LogFunc(Log{
Level: "INFO", Content: "Calculating binary properties", Prompt: false,
@ -646,14 +649,14 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
}
// Create the hash writer
sha512Hash := xxhash.New()
_, err = sha512Hash.Write(dataTemplateBytes)
xxHash := xxhash.New()
_, err = xxHash.Write(dataTemplateBytes)
if err != nil {
return err, nil
}
// Create a multi-writer so we can write to the file and the hash at the same time
multiWriter := io.MultiWriter(file, sha512Hash)
multiWriter := io.MultiWriter(file, xxHash)
// Create the ZStandard writer
writer, err := zstd.NewWriter(multiWriter, zstd.WithEncoderLevel(zstd.SpeedDefault))
@ -679,7 +682,7 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
})
// Sign the hash
signature := ed25519.Sign(privateKey, sha512Hash.Sum(nil))
signature := ed25519.Sign(privateKey, xxHash.Sum(nil))
publicKey := privateKey.Public().(ed25519.PublicKey)
// Write the signature and public key to the file
@ -690,13 +693,13 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
}
// Write the signature
_, err = file.WriteAt(signature, int64(ConstPackageEPKInitialByteOffset))
_, err = file.WriteAt(signature, ConstPackageEPKInitialByteOffset)
if err != nil {
return err, ErrPackageEPKCannotWriteFile
}
// Write the public key
_, err = file.WriteAt(publicKey, int64(ConstPackageEPKInitialByteOffset)+int64(SignatureLength))
_, err = file.WriteAt(publicKey, ConstPackageEPKInitialByteOffset+ConstPackageEPKSignatureLength)
if err != nil {
return err, ErrPackageEPKCannotWriteFile
}
@ -709,3 +712,276 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
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
}