diff --git a/gma/gma.go b/gma/gma.go new file mode 100644 index 0000000..5e894bc --- /dev/null +++ b/gma/gma.go @@ -0,0 +1,327 @@ +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 +} +*/