From 5fae89a394da2aa1da08e8f577f4e08dd193789a Mon Sep 17 00:00:00 2001 From: Andreas Bielawski Date: Wed, 22 Jun 2022 21:22:45 +0200 Subject: [PATCH] Initial commit --- .github/workflows/build.yml | 61 +++++++++++++++ .gitignore | 2 + LICENSE | 24 ++++++ README.md | 14 ++++ go.mod | 3 + main.go | 149 ++++++++++++++++++++++++++++++++++++ 6 files changed, 253 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 go.mod create mode 100644 main.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..3a57eb9 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,61 @@ +name: Build +on: + - push + - pull_request + +env: + GOVER: ^1.18 + NAME: stargazer + +jobs: + build: + name: build + runs-on: ubuntu-latest + strategy: + matrix: + GOOS: [ windows, linux, darwin ] + GOARCH: [ amd64, 386, arm, arm64 ] + exclude: + - GOOS: windows + GOARCH: arm + - GOOS: darwin + GOARCH: 386 + - GOOS: darwin + GOARCH: arm + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Cache + uses: actions/cache@v2 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ env.GOVER }} + + - name: Build + env: + GOOS: ${{ matrix.GOOS }} + GOARCH: ${{ matrix.GOARCH }} + run: go build -ldflags="-s -w" -o dist/$NAME-$GOOS-$GOARCH + + - name: Rename binaries (Windows) + if: matrix.GOOS == 'windows' + run: for x in dist/$NAME-windows-*; do mv $x $x.exe; done + + - name: Upload binary + uses: actions/upload-artifact@v3 + with: + name: ${{env.NAME}}-${{ matrix.GOOS }}-${{ matrix.GOARCH }}-${{github.sha}} + path: dist/* + retention-days: 90 + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cc2124 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.iml +.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..67981fe --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Stargazer + +Tool to extract STAR files from the PSX used by its package manager "PackmanJr". + +## Usage + +```bash +./stargazer stargazer [output dir (optional)] +``` + +If not 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`). + +## Credits +Thanks to @martravi for helping with reverse-engineering! diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..20abe3a --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/Brawl345/stargazer + +go 1.18 diff --git a/main.go b/main.go new file mode 100644 index 0000000..b746b0f --- /dev/null +++ b/main.go @@ -0,0 +1,149 @@ +package main + +import ( + "bytes" + "crypto/sha1" + "encoding/binary" + "flag" + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" +) + +const VERSION = "1.0" + +type ( + Header struct { + Unknown1stByte int8 + Unknown2ndByte int8 + Filesize uint32 + FilenameSize int8 + MaybePadding int8 + } + + 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 ParseEntry(file io.Reader) (*Entry, error) { + entry := Entry{} + err := binary.Read(file, binary.LittleEndian, &entry.Header.Unknown1stByte) + if err == io.EOF { + return nil, err + } + + binary.Read(file, binary.LittleEndian, &entry.Header.Unknown2ndByte) + + binary.Read(file, binary.LittleEndian, &entry.Header.Filesize) + + binary.Read(file, binary.LittleEndian, &entry.Header.FilenameSize) + binary.Read(file, binary.LittleEndian, &entry.Header.MaybePadding) + + 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 +} + +func main() { + fmt.Printf("Stargazer v%s\n", VERSION) + flag.Parse() + if flag.NArg() < 1 || flag.NArg() > 2 { + fmt.Println("Usage: stargazer [output dir (optional)]") + os.Exit(1) + } + inputFile := flag.Arg(0) + outputDir := flag.Arg(1) + 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!") +}