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 } */