mirror of
https://github.com/Brawl345/stargazer.git
synced 2024-06-01 23:38:44 +02:00
Rework CLI and add tests
This commit is contained in:
parent
5a713daee6
commit
9b9b06d47a
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
|
@ -42,10 +42,15 @@ jobs:
|
|||
with:
|
||||
go-version: ${{ env.GOVER }}
|
||||
|
||||
- name: Run tests
|
||||
working-directory: pkg/stargazer
|
||||
run: go test
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GOOS: ${{ matrix.GOOS }}
|
||||
GOARCH: ${{ matrix.GOARCH }}
|
||||
working-directory: cmd/stargazer
|
||||
run: go build -ldflags="-s -w" -o dist/$NAME-$GOOS-$GOARCH
|
||||
|
||||
- name: Rename binaries (Windows)
|
||||
|
|
10
CHANGES.md
Normal file
10
CHANGES.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
## Changelog
|
||||
|
||||
### 2.0.0 (Future)
|
||||
|
||||
* Reworked CLI
|
||||
* Can be used as a library
|
||||
|
||||
### 1.0.0 (2022-06-22)
|
||||
|
||||
* Initial release
|
65
README.md
65
README.md
|
@ -1,36 +1,61 @@
|
|||
# Stargazer
|
||||
|
||||
Tool to extract and repack STAR files from the PSX used by its package manager "PackmanJr".
|
||||
Library to handle STAR files from the PSX used by its package manager "PackmanJr". Comes with a CLI!
|
||||
|
||||
More info: <https://playstationdev.wiki/ps2devwiki/index.php/STAR_Files>
|
||||
|
||||
## Usage
|
||||
|
||||
### General usage
|
||||
|
||||
```txt
|
||||
Usage: stargazer <operation> <arguments>
|
||||
|
||||
To extract files:
|
||||
stargazer x <star file> [output dir (optional)]
|
||||
|
||||
To pack a folder:
|
||||
stargazer p <input dir> <star file>
|
||||
NAME:
|
||||
stargazer - A tool to handle PSX STAR files
|
||||
|
||||
USAGE:
|
||||
stargazer [global options] command [command options] [arguments...]
|
||||
|
||||
COMMANDS:
|
||||
unpack, u Unpacks files from a STAR file
|
||||
info, i Shows information about a STAR file
|
||||
help, h Shows a list of commands or help for one command
|
||||
|
||||
GLOBAL OPTIONS:
|
||||
--help, -h show help (default: false)
|
||||
--quiet, -q Do not print any messages (default: false)
|
||||
--version, -v print the version (default: false)
|
||||
```
|
||||
|
||||
### Unpack
|
||||
|
||||
```txt
|
||||
NAME:
|
||||
stargazer unpack - Unpacks files from a STAR file
|
||||
|
||||
USAGE:
|
||||
stargazer unpack [command options] [arguments...]
|
||||
|
||||
OPTIONS:
|
||||
--input value, -i value Path to STAR file
|
||||
--output value, -o value Path to output directory. Defaults to '<input file without .star>_extracted'
|
||||
```
|
||||
|
||||
If no output directory is given, the file is extracted to the file name minus the extension plus "`_extracted`" (
|
||||
e.g. `xPackmanJr_0.105.star` -> `xPackmanJr_0.105_extracted`). Same goes for packing (it will append `_packed.star`).
|
||||
|
||||
**NOTE:** Packing is experimental since I have no way to test it and I'm not sure about the limitations of the system (e.g. filenames). I also don't know if the order of the files is relevant.
|
||||
### Info
|
||||
|
||||
```txt
|
||||
NAME:
|
||||
stargazer info - Shows information about a STAR file
|
||||
|
||||
USAGE:
|
||||
stargazer info [command options] [arguments...]
|
||||
|
||||
OPTIONS:
|
||||
--input value, -i value Path to STAR file
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
Thanks to @martravi for helping with reverse-engineering!
|
||||
|
||||
## Changelog
|
||||
|
||||
### v2.0
|
||||
|
||||
- Add re-packing (experimental)
|
||||
|
||||
### v1.0
|
||||
|
||||
- Initial release
|
||||
Thanks to [@martravi](https://github.com/martravi) for helping with reverse-engineering!
|
||||
|
|
121
cmd/stargazer/main.go
Normal file
121
cmd/stargazer/main.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Brawl345/stargazer/pkg/stargazer"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var input string
|
||||
var output string
|
||||
var quiet bool
|
||||
|
||||
func main() {
|
||||
app := &cli.App{
|
||||
Name: "stargazer",
|
||||
Usage: "A tool to handle PSX STAR files",
|
||||
Version: "2.0.0",
|
||||
Suggest: true,
|
||||
EnableBashCompletion: true,
|
||||
Authors: []*cli.Author{
|
||||
{
|
||||
Name: "Brawl345",
|
||||
},
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
Aliases: []string{"q"},
|
||||
Usage: "Do not print any messages",
|
||||
Destination: &quiet,
|
||||
},
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "unpack",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Unpacks files from a STAR file",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "input",
|
||||
Aliases: []string{"i"},
|
||||
Required: true,
|
||||
Usage: "Path to STAR file",
|
||||
Destination: &input,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "output",
|
||||
Aliases: []string{"o"},
|
||||
Required: false,
|
||||
Usage: "Path to output directory. Defaults to '<input file without .star>_extracted'",
|
||||
Destination: &output,
|
||||
},
|
||||
},
|
||||
Action: unpack,
|
||||
},
|
||||
{
|
||||
Name: "info",
|
||||
Aliases: []string{"i"},
|
||||
Usage: "Shows information about a STAR file",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "input",
|
||||
Aliases: []string{"i"},
|
||||
Required: true,
|
||||
Usage: "Path to STAR file",
|
||||
Destination: &input,
|
||||
},
|
||||
},
|
||||
Action: info,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
fmt.Println("ERROR: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func unpack(_ *cli.Context) error {
|
||||
star, err := stargazer.LoadSTARFromFile(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if output == "" {
|
||||
output = fmt.Sprintf("%s_extracted", filepath.Base(strings.TrimSuffix(input, filepath.Ext(input))))
|
||||
}
|
||||
|
||||
if !quiet {
|
||||
log.Printf("Will unpack to '%s'", output)
|
||||
}
|
||||
|
||||
for _, entry := range star.Entries {
|
||||
if !quiet {
|
||||
log.Printf("Unpacking '%s'...\n", entry.Filename)
|
||||
}
|
||||
err := entry.Unpack(output)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func info(_ *cli.Context) error {
|
||||
star, err := stargazer.LoadSTARFromFile(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(star.Info())
|
||||
return nil
|
||||
}
|
7
go.mod
7
go.mod
|
@ -1,3 +1,10 @@
|
|||
module github.com/Brawl345/stargazer
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.10.3 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
)
|
||||
|
|
8
go.sum
Normal file
8
go.sum
Normal file
|
@ -0,0 +1,8 @@
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/urfave/cli/v2 v2.10.3 h1:oi571Fxz5aHugfBAJd5nkwSk3fzATXtMlpxdLylSCMo=
|
||||
github.com/urfave/cli/v2 v2.10.3/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
141
main.go
141
main.go
|
@ -1,141 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const VERSION = "2.0"
|
||||
|
||||
func usage() {
|
||||
fmt.Println("Usage: stargazer <operation> <arguments>")
|
||||
fmt.Println("")
|
||||
fmt.Println(" To extract files:")
|
||||
fmt.Println(" stargazer x <star file> [output dir (optional)]")
|
||||
fmt.Println("")
|
||||
fmt.Println(" To pack a folder:")
|
||||
fmt.Println(" stargazer p <input dir> <star file>")
|
||||
fmt.Println("")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func extract() {
|
||||
inputFile := flag.Arg(1)
|
||||
outputDir := flag.Arg(2)
|
||||
if outputDir == "" {
|
||||
outputDir = fmt.Sprintf("%s_extracted", filepath.Base(strings.TrimSuffix(inputFile, filepath.Ext(inputFile))))
|
||||
}
|
||||
|
||||
file, err := os.Open(inputFile)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
var star Star
|
||||
|
||||
log.Printf("Parsing '%s'...\n", inputFile)
|
||||
for {
|
||||
entry, err := ParseEntry(file)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
star.Entries = append(star.Entries, *entry)
|
||||
}
|
||||
|
||||
log.Printf("Extracting to '%s'...\n", outputDir)
|
||||
for _, entry := range star.Entries {
|
||||
log.Printf("Extracting %s...\n", entry.GetFileName())
|
||||
err := entry.Extract(outputDir)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Extraction complete!")
|
||||
}
|
||||
|
||||
func pack() {
|
||||
log.Printf("WARNING!!! Packing is experimental and may not work properly!\n")
|
||||
inputDir := flag.Arg(1)
|
||||
outputFile := flag.Arg(2)
|
||||
if outputFile == "" {
|
||||
outputFile = fmt.Sprintf("%s_packed.star", filepath.Base(inputDir))
|
||||
}
|
||||
|
||||
log.Println("Reading files...")
|
||||
var star Star
|
||||
err := filepath.Walk(inputDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
entry := Entry{}
|
||||
fp := strings.TrimPrefix(path, inputDir)
|
||||
fp = strings.TrimPrefix(fp, string(os.PathSeparator))
|
||||
entry.FileName = []byte(strings.ReplaceAll(fp, "\\", "/"))
|
||||
entry.Content, err = ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry.Header.Headersize = uint8(8 + len(entry.GetFileName()))
|
||||
entry.Header.Filesize = uint32(len(entry.Content))
|
||||
entry.Header.FilenameSize = uint8(len(entry.GetFileName()))
|
||||
|
||||
copy(entry.Sha1[:], entry.CalculateSha1())
|
||||
star.Entries = append(star.Entries, entry)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
log.Println("Packing...")
|
||||
file, err := os.Create(outputFile)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
for _, entry := range star.Entries {
|
||||
log.Printf("Packing %s...\n", entry.GetFileName())
|
||||
err := entry.Pack(file)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
log.Println("Packing complete!")
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Printf("Stargazer v%s\n", VERSION)
|
||||
flag.Parse()
|
||||
if flag.NArg() < 1 || flag.NArg() > 3 {
|
||||
usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
operation := flag.Arg(0)
|
||||
switch operation {
|
||||
case "x":
|
||||
extract()
|
||||
case "p":
|
||||
pack()
|
||||
default:
|
||||
usage()
|
||||
}
|
||||
}
|
20
pkg/stargazer/errors.go
Normal file
20
pkg/stargazer/errors.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package stargazer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var ErrNoEntries = errors.New("invalid STAR file - no file entries found")
|
||||
|
||||
type (
|
||||
ErrSHA1Mismatch struct {
|
||||
Expected string
|
||||
Actual string
|
||||
Filename string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrSHA1Mismatch) Error() string {
|
||||
return fmt.Sprintf("SHA1 mismatch on file '%s': expected %s, actual %s", e.Filename, e.Expected, e.Actual)
|
||||
}
|
88
pkg/stargazer/star.go
Normal file
88
pkg/stargazer/star.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package stargazer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
Star struct {
|
||||
Entries []Entry
|
||||
}
|
||||
|
||||
Entry struct {
|
||||
Header
|
||||
Content []byte
|
||||
SHA1 [20]byte
|
||||
}
|
||||
|
||||
Header struct {
|
||||
Headersize uint8
|
||||
Padding1 uint8
|
||||
Filesize uint32
|
||||
FilenameSize uint8
|
||||
Padding2 uint8
|
||||
Filename string
|
||||
}
|
||||
)
|
||||
|
||||
func (e *Entry) SHA1String() string {
|
||||
return fmt.Sprintf("%x", e.SHA1)
|
||||
}
|
||||
|
||||
func (e *Entry) Unpack(outputDir string) error {
|
||||
fp := filepath.Join(outputDir, e.Filename)
|
||||
err := os.MkdirAll(filepath.Dir(fp), os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(fp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Write(e.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Entry) Info() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%s\n", e.Filename))
|
||||
sb.WriteString(fmt.Sprintf(" Filesize: %d bytes\n", e.Header.Filesize))
|
||||
sb.WriteString(fmt.Sprintf(" SHA1: %s\n", e.SHA1String()))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (s *Star) Unpack(outputDir string) error {
|
||||
for _, e := range s.Entries {
|
||||
err := e.Unpack(outputDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Star) Info() string {
|
||||
var sb strings.Builder
|
||||
|
||||
var contentSize uint64
|
||||
for _, e := range s.Entries {
|
||||
contentSize += uint64(e.Header.Filesize)
|
||||
sb.WriteString(e.Info())
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("Total contents: %d\n", len(s.Entries)))
|
||||
sb.WriteString(fmt.Sprintf("Total content size: %d bytes\n", contentSize))
|
||||
|
||||
return sb.String()
|
||||
}
|
124
pkg/stargazer/stargazer.go
Normal file
124
pkg/stargazer/stargazer.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package stargazer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func parseEntry(file io.Reader) (*Entry, error) {
|
||||
entry := Entry{}
|
||||
err := binary.Read(file, binary.LittleEndian, &entry.Header.Headersize)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return nil, fmt.Errorf("error reading headersize: %v", err)
|
||||
}
|
||||
|
||||
err = binary.Read(file, binary.LittleEndian, &entry.Header.Padding1)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return nil, fmt.Errorf("error reading padding1: %v", err)
|
||||
}
|
||||
|
||||
err = binary.Read(file, binary.LittleEndian, &entry.Header.Filesize)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return nil, fmt.Errorf("error reading filesize: %v", err)
|
||||
}
|
||||
|
||||
err = binary.Read(file, binary.LittleEndian, &entry.Header.FilenameSize)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return nil, fmt.Errorf("error reading filename size: %v", err)
|
||||
}
|
||||
|
||||
err = binary.Read(file, binary.LittleEndian, &entry.Header.Padding2)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return nil, fmt.Errorf("error reading padding2: %v", err)
|
||||
}
|
||||
|
||||
filename := make([]byte, entry.Header.FilenameSize)
|
||||
err = binary.Read(file, binary.LittleEndian, &filename)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return nil, fmt.Errorf("error reading filename: %v", err)
|
||||
}
|
||||
entry.Header.Filename = string(filename)
|
||||
|
||||
entry.Content = make([]byte, entry.Header.Filesize)
|
||||
err = binary.Read(file, binary.LittleEndian, &entry.Content)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return nil, fmt.Errorf("error reading content: %v", err)
|
||||
}
|
||||
|
||||
err = binary.Read(file, binary.LittleEndian, &entry.SHA1)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return nil, fmt.Errorf("error reading sha1: %v", err)
|
||||
}
|
||||
|
||||
h := sha1.New()
|
||||
h.Write(entry.Content)
|
||||
calculatedHash := h.Sum(nil)
|
||||
|
||||
if !bytes.Equal(calculatedHash, entry.SHA1[:]) {
|
||||
return nil, ErrSHA1Mismatch{
|
||||
Filename: entry.Header.Filename,
|
||||
Expected: hex.EncodeToString(entry.SHA1[:]),
|
||||
Actual: hex.EncodeToString(calculatedHash),
|
||||
}
|
||||
}
|
||||
|
||||
return &entry, nil
|
||||
}
|
||||
|
||||
func LoadSTARFromFile(fp string) (*Star, error) {
|
||||
file, err := os.Open(fp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
return LoadSTAR(file)
|
||||
}
|
||||
|
||||
func LoadSTAR(file io.Reader) (*Star, error) {
|
||||
star := &Star{}
|
||||
for {
|
||||
entry, err := parseEntry(file)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
star.Entries = append(star.Entries, *entry)
|
||||
}
|
||||
|
||||
if len(star.Entries) == 0 {
|
||||
return nil, ErrNoEntries
|
||||
}
|
||||
|
||||
return star, nil
|
||||
}
|
203
pkg/stargazer/stargazer_test.go
Normal file
203
pkg/stargazer/stargazer_test.go
Normal file
|
@ -0,0 +1,203 @@
|
|||
package stargazer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadSTARFromFile(t *testing.T) {
|
||||
got, err := LoadSTARFromFile(filepath.Join("..", "..", "testdata", "testfile.star"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(got.Entries) != 3 {
|
||||
t.Errorf("Expected 3 entries, got %d", len(got.Entries))
|
||||
return
|
||||
}
|
||||
|
||||
// Check first entry
|
||||
if got.Entries[0].Header.Headersize != 22 {
|
||||
t.Errorf("Expected header size of first entry to be 22 bytes long, got %d", got.Entries[0].Header.Headersize)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[0].Header.Padding1 != 0 {
|
||||
t.Errorf("Expected first padding of first entry to be 0, got %d", got.Entries[0].Header.Padding1)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[0].Header.Filesize != 10928 {
|
||||
t.Errorf("Expected filesize of first entry to be 10928, got %d", got.Entries[0].Header.Filesize)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[0].Header.FilenameSize != 14 {
|
||||
t.Errorf("Expected filename size of first entry to be 14, got %d", got.Entries[0].Header.FilenameSize)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[0].Header.Padding2 != 0 {
|
||||
t.Errorf("Expected second padding of first entry to be 0, got %d", got.Entries[0].Header.Padding2)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[0].Header.Filename != "NulledFile.rel" {
|
||||
t.Errorf("Expected filename of first entry to be 'NulledFile.rel', got '%s'", got.Entries[0].Header.Filename)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[0].Content == nil {
|
||||
t.Errorf("Expected content of first entry to be non-nil")
|
||||
return
|
||||
}
|
||||
|
||||
if uint32(len(got.Entries[0].Content)) != got.Entries[0].Header.Filesize {
|
||||
t.Errorf("Expected content of first entry to be %d bytes long, got %d", got.Entries[0].Header.Filesize, len(got.Entries[0].Content))
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[0].SHA1String() != "3d433fcbe9585b05ea877814bad60774ff8a9e7c" {
|
||||
t.Errorf("Expected SHA1 of first entry to be '3d433fcbe9585b05ea877814bad60774ff8a9e7c', got '%s'", got.Entries[0].SHA1String())
|
||||
return
|
||||
}
|
||||
|
||||
// Check second entry
|
||||
if got.Entries[1].Header.Headersize != 20 {
|
||||
t.Errorf("Expected header size of second entry to be 22 bytes long, got %d", got.Entries[1].Header.Headersize)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[1].Header.Padding1 != 0 {
|
||||
t.Errorf("Expected first padding of second entry to be 0, got %d", got.Entries[1].Header.Padding1)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[1].Header.Filesize != 313 {
|
||||
t.Errorf("Expected filesize of second entry to be 313, got %d", got.Entries[1].Header.Filesize)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[1].Header.FilenameSize != 12 {
|
||||
t.Errorf("Expected filename size of second entry to be 12, got %d", got.Entries[1].Header.FilenameSize)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[1].Header.Padding2 != 0 {
|
||||
t.Errorf("Expected second padding of second entry to be 0, got %d", got.Entries[1].Header.Padding2)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[1].Header.Filename != "metadata.xml" {
|
||||
t.Errorf("Expected filename of second entry to be 'metadata.xml', got '%s'", got.Entries[1].Header.Filename)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[1].Content == nil {
|
||||
t.Errorf("Expected content of second entry to be non-nil")
|
||||
return
|
||||
}
|
||||
|
||||
if uint32(len(got.Entries[1].Content)) != got.Entries[1].Header.Filesize {
|
||||
t.Errorf("Expected content of second entry to be %d bytes long, got %d", got.Entries[1].Header.Filesize, len(got.Entries[1].Content))
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[1].SHA1String() != "2e59ec1846a50fb75042c6786299d13d8f5e39b6" {
|
||||
t.Errorf("Expected SHA1 of second entry to be '2e59ec1846a50fb75042c6786299d13d8f5e39b6', got '%s'", got.Entries[1].SHA1String())
|
||||
return
|
||||
}
|
||||
|
||||
// Check third entry
|
||||
if got.Entries[2].Header.Headersize != 19 {
|
||||
t.Errorf("Expected header size of third entry to be 19 bytes long, got %d", got.Entries[2].Header.Headersize)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[2].Header.Padding1 != 0 {
|
||||
t.Errorf("Expected first padding of third entry to be 0, got %d", got.Entries[2].Header.Padding1)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[2].Header.Filesize != 411 {
|
||||
t.Errorf("Expected filesize of third entry to be 411, got %d", got.Entries[2].Header.Filesize)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[2].Header.FilenameSize != 11 {
|
||||
t.Errorf("Expected filename size of third entry to be 11, got %d", got.Entries[2].Header.FilenameSize)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[2].Header.Padding2 != 0 {
|
||||
t.Errorf("Expected second padding of third entry to be 0, got %d", got.Entries[2].Header.Padding2)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[2].Header.Filename != "install.txt" {
|
||||
t.Errorf("Expected filename of third entry to be 'install.txt', got '%s'", got.Entries[2].Header.Filename)
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[2].Content == nil {
|
||||
t.Errorf("Expected content of third entry to be non-nil")
|
||||
return
|
||||
}
|
||||
|
||||
if uint32(len(got.Entries[2].Content)) != got.Entries[2].Header.Filesize {
|
||||
t.Errorf("Expected content of third entry to be %d bytes long, got %d", got.Entries[2].Header.Filesize, len(got.Entries[2].Content))
|
||||
return
|
||||
}
|
||||
|
||||
if got.Entries[2].SHA1String() != "6c5768c3c82a174f0ea264c1c0e80450648da4c5" {
|
||||
t.Errorf("Expected SHA1 of third entry to be '6c5768c3c82a174f0ea264c1c0e80450648da4c5', got '%s'", got.Entries[2].SHA1String())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSTARFromFileFail(t *testing.T) {
|
||||
_, err := LoadSTARFromFile("invalid.star")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestStar_Unpack(t *testing.T) {
|
||||
got, err := LoadSTARFromFile(filepath.Join("..", "..", "testdata", "testfile.star"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
outputDir := t.TempDir()
|
||||
err = got.Unpack(outputDir)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !fileExists(filepath.Join(outputDir, "NulledFile.rel")) {
|
||||
t.Errorf("Expected file 'NulledFile.rel' to exist in output directory")
|
||||
return
|
||||
}
|
||||
|
||||
if !fileExists(filepath.Join(outputDir, "metadata.xml")) {
|
||||
t.Errorf("Expected file 'metadata.xml' to exist in output directory")
|
||||
return
|
||||
}
|
||||
|
||||
if !fileExists(filepath.Join(outputDir, "install.txt")) {
|
||||
t.Errorf("Expected file 'install.txt' to exist in output directory")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func fileExists(fileName string) bool {
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
118
star.go
118
star.go
|
@ -1,118 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type (
|
||||
Header struct {
|
||||
Headersize uint8
|
||||
Padding1 uint8
|
||||
Filesize uint32
|
||||
FilenameSize uint8
|
||||
Padding2 uint8
|
||||
}
|
||||
|
||||
Entry struct {
|
||||
Header
|
||||
FileName []byte
|
||||
Content []byte
|
||||
Sha1 [20]byte
|
||||
}
|
||||
|
||||
Star struct {
|
||||
Entries []Entry
|
||||
}
|
||||
)
|
||||
|
||||
func (e *Entry) GetFileName() string {
|
||||
return string(e.FileName[:])
|
||||
}
|
||||
|
||||
func (e *Entry) CalculateSha1() []byte {
|
||||
h := sha1.New()
|
||||
h.Write(e.Content)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func (e *Entry) GetSha1() string {
|
||||
return fmt.Sprintf("%x", e.Sha1)
|
||||
}
|
||||
|
||||
func (e *Entry) Extract(outputDir string) error {
|
||||
fp := filepath.Join(outputDir, e.GetFileName())
|
||||
err := os.MkdirAll(filepath.Dir(fp), os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(fp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Write(e.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Entry) Pack(file *os.File) error {
|
||||
err := binary.Write(file, binary.LittleEndian, e.Header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = file.Write(e.FileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = file.Write(e.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = file.Write(e.Sha1[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseEntry(file io.Reader) (*Entry, error) {
|
||||
entry := Entry{}
|
||||
err := binary.Read(file, binary.LittleEndian, &entry.Header.Headersize)
|
||||
if err == io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
binary.Read(file, binary.LittleEndian, &entry.Header.Padding1)
|
||||
|
||||
binary.Read(file, binary.LittleEndian, &entry.Header.Filesize)
|
||||
|
||||
binary.Read(file, binary.LittleEndian, &entry.Header.FilenameSize)
|
||||
binary.Read(file, binary.LittleEndian, &entry.Header.Padding2)
|
||||
|
||||
filename := make([]byte, entry.Header.FilenameSize)
|
||||
binary.Read(file, binary.LittleEndian, &filename)
|
||||
entry.FileName = filename
|
||||
|
||||
entry.Content = make([]byte, entry.Header.Filesize)
|
||||
binary.Read(file, binary.LittleEndian, &entry.Content)
|
||||
|
||||
binary.Read(file, binary.LittleEndian, &entry.Sha1)
|
||||
|
||||
calculatedHash := entry.CalculateSha1()
|
||||
|
||||
if !bytes.Equal(calculatedHash, entry.Sha1[:]) {
|
||||
log.Fatalln("Hash mismatch")
|
||||
}
|
||||
|
||||
return &entry, nil
|
||||
}
|
BIN
testdata/testfile.star
vendored
Normal file
BIN
testdata/testfile.star
vendored
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user