You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
328 lines
7.9 KiB
Go
328 lines
7.9 KiB
Go
package gma
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"io"
|
|
"os"
|
|
)
|
|
|
|
type AddonArchive struct {
|
|
LZMA bool
|
|
}
|
|
type GMAReader struct {
|
|
FileHandle *os.File
|
|
//gmaStream io.Reader
|
|
gmaStreamReader *bufio.Reader
|
|
cursorOffset uint32
|
|
//h header
|
|
}
|
|
type GMAHeader struct {
|
|
FormatVersion byte
|
|
SteamID uint64
|
|
Timestamp uint64
|
|
|
|
Title string
|
|
Description string
|
|
Author string
|
|
|
|
AddonVersion int32
|
|
}
|
|
type GMAFileMetadata struct {
|
|
FileNumber int32
|
|
FileName string
|
|
Offset int64
|
|
FileSize int64
|
|
CRC uint32
|
|
NextType int32
|
|
}
|
|
type GMAExtractionMeta struct {
|
|
OriginalMeta GMAFileMetadata
|
|
ExtractedCRC uint32
|
|
ExtractedSHA256 string
|
|
}
|
|
|
|
func NewReader(fileName string) (_ GMAReader, err error) {
|
|
return GMAReader{}.NewReader(fileName)
|
|
}
|
|
func (r GMAReader) NewReader(fileName string) (_ GMAReader, err error) {
|
|
r.FileHandle, err = os.Open(fileName)
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
r.gmaStreamReader = bufio.NewReader(r.FileHandle)
|
|
return r, nil
|
|
}
|
|
|
|
func (r *GMAReader) IsValidGMAD() (isValid bool, err error) {
|
|
gmadHeader, err := r.gmaStreamReader.Peek(4)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return string(gmadHeader) == "GMAD", nil
|
|
}
|
|
func (r *GMAReader) IsCompressed() (isValid bool, err error) {
|
|
gmadHeader, err := r.gmaStreamReader.Peek(4)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return gmadHeader[0] == 93 && gmadHeader[1] == 0 && gmadHeader[1] == gmadHeader[2] && gmadHeader[1] == gmadHeader[3], nil
|
|
}
|
|
|
|
func (r *GMAReader) Close() {
|
|
r.FileHandle.Close()
|
|
}
|
|
|
|
func (r *GMAReader) ReadHeader() (GMAHeader, error) {
|
|
header := GMAHeader{}
|
|
|
|
r.gmaStreamReader.Discard(4)
|
|
r.cursorOffset += 4
|
|
if r.cursorOffset != 4 {
|
|
return header, fmt.Errorf("%d invalid offset", r.cursorOffset)
|
|
}
|
|
|
|
// Read the format version
|
|
formatVersion, err := r.gmaStreamReader.ReadByte()
|
|
if err != nil {
|
|
return header, err
|
|
}
|
|
r.cursorOffset++
|
|
|
|
// Read the SteamID
|
|
steamIDBytes := make([]byte, 8)
|
|
_, err = r.gmaStreamReader.Read(steamIDBytes)
|
|
if err != nil {
|
|
return header, err
|
|
}
|
|
r.cursorOffset += 8
|
|
header.SteamID = binary.LittleEndian.Uint64(steamIDBytes)
|
|
|
|
// Read the Timestamp
|
|
timestampBytes := make([]byte, 8)
|
|
_, err = r.gmaStreamReader.Read(timestampBytes)
|
|
if err != nil {
|
|
return header, err
|
|
}
|
|
r.cursorOffset += 8
|
|
header.Timestamp = binary.LittleEndian.Uint64(timestampBytes)
|
|
|
|
if formatVersion > 1 {
|
|
_, err = r.gmaStreamReader.Discard(1)
|
|
if err != nil {
|
|
return header, err
|
|
}
|
|
r.cursorOffset++
|
|
}
|
|
|
|
// Read the Title
|
|
header.Title, err = r.gmaStreamReader.ReadString(byte(0))
|
|
if err != nil {
|
|
return header, err
|
|
}
|
|
header.Title = header.Title[:len(header.Title)-1] // remove nullbyte
|
|
r.cursorOffset += uint32(len(header.Title) + 1) // Add title length + null byte
|
|
|
|
// Read the Description
|
|
header.Description, err = r.gmaStreamReader.ReadString(byte(0))
|
|
if err != nil {
|
|
return header, err
|
|
}
|
|
header.Description = header.Description[:len(header.Description)-1] // remove nullbyte
|
|
//fmt.Printf("Desc Start %d\n", r.cursorOffset)
|
|
r.cursorOffset += uint32(len(header.Description) + 1) // Add description length + null byte
|
|
//fmt.Printf("Desc End %d\n", r.cursorOffset)
|
|
|
|
// Read the Author
|
|
header.Author, err = r.gmaStreamReader.ReadString(byte(0))
|
|
if err != nil {
|
|
return header, err
|
|
}
|
|
header.Author = header.Author[:len(header.Author)-1] // remove nullbyte
|
|
r.cursorOffset += uint32(len(header.Author) + 1) // Add author length + null byte
|
|
|
|
// Read the AddonVersion
|
|
addonVersionBytes := make([]byte, 4)
|
|
_, err = r.gmaStreamReader.Read(addonVersionBytes)
|
|
if err != nil {
|
|
return header, err
|
|
}
|
|
r.cursorOffset += 4
|
|
header.AddonVersion = int32(binary.LittleEndian.Uint32(addonVersionBytes))
|
|
|
|
return header, nil
|
|
}
|
|
|
|
func (r *GMAReader) readFileMetadata() (GMAFileMetadata, error) {
|
|
metadata := GMAFileMetadata{}
|
|
|
|
// Read the file name
|
|
fileName, err := r.gmaStreamReader.ReadString(byte(0))
|
|
if err != nil {
|
|
return metadata, err
|
|
}
|
|
fileName = fileName[:len(fileName)-1] // remove nullbyte
|
|
r.cursorOffset += uint32(len(fileName) + 1) // Add name length + null byte
|
|
metadata.FileName = fileName
|
|
|
|
// Read the file size
|
|
fileSizeBytes := make([]byte, 8)
|
|
_, err = r.gmaStreamReader.Read(fileSizeBytes)
|
|
if err != nil {
|
|
return metadata, err
|
|
}
|
|
r.cursorOffset += 8
|
|
metadata.FileSize = int64(binary.LittleEndian.Uint64(fileSizeBytes))
|
|
|
|
// Read the file crc
|
|
crcBytes := make([]byte, 4)
|
|
_, err = r.gmaStreamReader.Read(crcBytes)
|
|
if err != nil {
|
|
return metadata, err
|
|
}
|
|
r.cursorOffset += 4
|
|
metadata.CRC = binary.LittleEndian.Uint32(crcBytes)
|
|
|
|
// Read the next type
|
|
nextTypeBytes := make([]byte, 4)
|
|
_, err = r.gmaStreamReader.Read(nextTypeBytes)
|
|
if err != nil {
|
|
return metadata, err
|
|
}
|
|
r.cursorOffset += 4
|
|
metadata.NextType = int32(binary.LittleEndian.Uint32(nextTypeBytes))
|
|
|
|
return metadata, nil
|
|
}
|
|
|
|
func (r *GMAReader) ReadFiles() (files []GMAFileMetadata, err error) {
|
|
// read nType 4byte
|
|
firstTypeBytes := make([]byte, 4)
|
|
_, err = r.gmaStreamReader.Read(firstTypeBytes)
|
|
if err != nil {
|
|
return files, err
|
|
}
|
|
r.cursorOffset += 4
|
|
firstType := int32(binary.LittleEndian.Uint32(firstTypeBytes))
|
|
|
|
if firstType == 0 {
|
|
return files, nil
|
|
}
|
|
fileOffset := int64(0)
|
|
fileNumber := int32(1)
|
|
for {
|
|
fileMeta, err := r.readFileMetadata()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return files, err
|
|
}
|
|
fileMeta.FileNumber = fileNumber
|
|
fileMeta.Offset = fileOffset
|
|
//fmt.Printf("%s CRC: %d Offset: %d Size: %d\n", fileMeta.FileName, fileMeta.CRC, fileMeta.Offset, fileMeta.FileSize)
|
|
//fmt.Printf("[% x]\n", fileMeta.FileName)
|
|
files = append(files, fileMeta)
|
|
fileOffset += fileMeta.FileSize
|
|
fileNumber++
|
|
if fileMeta.NextType == 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
return files, nil
|
|
}
|
|
func (r *GMAReader) GetOffset() (offset uint32) {
|
|
return r.cursorOffset
|
|
}
|
|
func (r *GMAReader) ExtractFileTo(fileMeta GMAFileMetadata, writer io.Writer) (extractMeta GMAExtractionMeta, err error) {
|
|
extractMeta.OriginalMeta = fileMeta
|
|
// Seek to the specified offset in the reader
|
|
limitReader := io.NewSectionReader(r.FileHandle, int64(r.cursorOffset)+fileMeta.Offset, int64(fileMeta.FileSize))
|
|
// Copy the specified length of data from the reader to the output file
|
|
buf := bytes.NewBuffer(nil)
|
|
_, err = io.CopyN(buf, limitReader, int64(fileMeta.FileSize))
|
|
if err != nil {
|
|
return extractMeta, err
|
|
}
|
|
shaHasher := sha256.New()
|
|
|
|
extractMeta.ExtractedCRC = crc32.Checksum(buf.Bytes(), crc32.MakeTable(crc32.IEEE))
|
|
shaHasher.Write(buf.Bytes())
|
|
extractMeta.ExtractedSHA256 = fmt.Sprintf("%x", shaHasher.Sum(nil))
|
|
buf.WriteTo(writer)
|
|
|
|
return extractMeta, nil
|
|
}
|
|
|
|
/*
|
|
func (r GMAReader) ReadHeader() (h GMAHeader, err error) {
|
|
gmaHeader := GMAHeader{}
|
|
|
|
r.readerStream.Discard(4) // skip header
|
|
gmaHeader.FormatVersion, err = r.readerStream.ReadByte()
|
|
if err != nil {
|
|
return gmaHeader, err
|
|
}
|
|
|
|
longBytes := make([]byte, 8)
|
|
wordBytes := make([]byte, 4)
|
|
|
|
// SteamID
|
|
bytesRead, err := r.readerStream.Read(longBytes)
|
|
if err != nil {
|
|
return gmaHeader, err
|
|
}
|
|
if bytesRead != 8 {
|
|
return gmaHeader, fmt.Errorf("steamid missing bytes")
|
|
}
|
|
gmaHeader.SteamID = binary.LittleEndian.Uint64(longBytes)
|
|
|
|
// Timestamp
|
|
bytesRead, err = r.readerStream.Read(longBytes)
|
|
if err != nil {
|
|
return gmaHeader, err
|
|
}
|
|
if bytesRead != 8 {
|
|
return gmaHeader, fmt.Errorf("timestamp missing bytes")
|
|
}
|
|
gmaHeader.Timestamp = binary.LittleEndian.Uint64(longBytes)
|
|
|
|
if gmaHeader.FormatVersion > 1 {
|
|
r.readerStream.Discard(1)
|
|
}
|
|
|
|
// Title
|
|
gmaHeader.Title, err = r.readString()
|
|
if err != nil {
|
|
return gmaHeader, err
|
|
}
|
|
// Description
|
|
gmaHeader.Description, err = r.readString()
|
|
if err != nil {
|
|
return gmaHeader, err
|
|
}
|
|
// Author
|
|
gmaHeader.Author, err = r.readString()
|
|
if err != nil {
|
|
return gmaHeader, err
|
|
}
|
|
|
|
// AddonVersion
|
|
bytesRead, err = r.readerStream.Read(wordBytes)
|
|
if err != nil {
|
|
return gmaHeader, err
|
|
}
|
|
if bytesRead != 4 {
|
|
return gmaHeader, fmt.Errorf("AddonVersion missing bytes")
|
|
}
|
|
gmaHeader.AddonVersion = int32(binary.LittleEndian.Uint32(wordBytes))
|
|
|
|
return gmaHeader, nil
|
|
}
|
|
*/
|