diff --git a/README.md b/README.md index 80ff652..4ef1ef9 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,36 @@ # Stargazer -Tool to extract STAR files from the PSX used by its package manager "PackmanJr". +Tool to extract and repack STAR files from the PSX used by its package manager "PackmanJr". + +More info: ## Usage -```bash -stargazer [output dir (optional)] +```txt +Usage: stargazer + + To extract files: + stargazer x [output dir (optional)] + + To pack a folder: + stargazer p ``` -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`). +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. ## Credits + Thanks to @martravi for helping with reverse-engineering! + +## Changelog + +### v2.0 + +- Add re-packing (experimental) + +### v1.0 + +- Initial release diff --git a/main.go b/main.go index b746b0f..d866f1a 100644 --- a/main.go +++ b/main.go @@ -1,115 +1,33 @@ package main import ( - "bytes" - "crypto/sha1" - "encoding/binary" "flag" "fmt" "io" + "io/ioutil" "log" "os" "path/filepath" "strings" ) -const VERSION = "1.0" +const VERSION = "2.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 usage() { + fmt.Println("Usage: stargazer ") + fmt.Println("") + fmt.Println(" To extract files:") + fmt.Println(" stargazer x [output dir (optional)]") + fmt.Println("") + fmt.Println(" To pack a folder:") + fmt.Println(" stargazer p ") + fmt.Println("") + os.Exit(1) } -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) +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)))) } @@ -147,3 +65,77 @@ func main() { 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() + } +} diff --git a/star.go b/star.go new file mode 100644 index 0000000..a5898f6 --- /dev/null +++ b/star.go @@ -0,0 +1,118 @@ +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 +}