From 8718c8297b5b28b23c6dad1ed12d8ecd70315358 Mon Sep 17 00:00:00 2001 From: Galaco Date: Sun, 15 Sep 2019 14:53:15 +0900 Subject: [PATCH 1/2] Add ReadHeader only, made errors comparable --- go.mod | 2 ++ reader.go | 48 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index a9d1ec4..1084ee6 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module github.com/galaco/vtf + +go 1.13 diff --git a/reader.go b/reader.go index 06f467d..c74946d 100644 --- a/reader.go +++ b/reader.go @@ -9,14 +9,38 @@ import ( "io" ) -const headerSize = 96 +const ( + headerSize = 96 + vtfSignature = "VTF\x00" +) + +var ( + // ErrorVtfSignatureMismatch occurs when a stream does not start with the VTF magic signature + ErrorVtfSignatureMismatch = errors.New("header signature does not match: VTF\x00") + // ErrorTextureDepthNotSupported occurs when attempting to parse a stream with depth > 1 + ErrorTextureDepthNotSupported = errors.New("only vtf textures with depth 1 are supported") + // ErrorMipmapSizeMismatch occurs when filesize does not match calculated mipmap size + ErrorMipmapSizeMismatch = errors.New("expected data size is smaller than actual") +) -// Reader: Vtf Reader +// Reader reads from a vtf stream type Reader struct { stream io.Reader } -// Read: Reads the vtf image from stream into a usable structure +// ReadHeader reads the header of a texture only. +func (reader *Reader) ReadHeader() (*Header, error) { + buf := bytes.Buffer{} + _, err := buf.ReadFrom(reader.stream) + if err != nil { + return nil, err + } + + // Header + return reader.parseHeader(buf.Bytes()) +} + +// Read parses vtf image from stream into a usable structure // The only error to expect would be if mipmap data size overflows the total file size; normally // due to tampered Header data. func (reader *Reader) Read() (*Vtf, error) { @@ -32,7 +56,7 @@ func (reader *Reader) Read() (*Vtf, error) { return nil, err } - // Tesources - in vtf 7.3+ only + // Resources - in vtf 7.3+ only resourceData, err := reader.parseOtherResourceData(header, buf.Bytes()) if err != nil { return nil, err @@ -58,7 +82,7 @@ func (reader *Reader) Read() (*Vtf, error) { }, nil } -// parseHeader: Parse vtf Header. +// parseHeader reads vtf Header. func (reader *Reader) parseHeader(buffer []byte) (*Header, error) { // We know Header is 96 bytes @@ -79,14 +103,14 @@ func (reader *Reader) parseHeader(buffer []byte) (*Header, error) { if err != nil { return nil, err } - if string(header.Signature[:4]) != "VTF\x00" { - return nil, errors.New("Header signature does not match: VTF\x00") + if string(header.Signature[:4]) != vtfSignature { + return nil, ErrorVtfSignatureMismatch } return &header, nil } -// parseOtherResourceData: Returns resource data for 7.3+ images +// parseOtherResourceData reads resource data for 7.3+ images func (reader *Reader) parseOtherResourceData(header *Header, buffer []byte) ([]byte, error) { // validate Header version if (header.Version[0]*10+header.Version[1] < 73) || header.NumResource == 0 { @@ -98,7 +122,7 @@ func (reader *Reader) parseOtherResourceData(header *Header, buffer []byte) ([]b return []byte{}, nil } -// readLowResolutionMipmap: Reads the low resolution texture information +// readLowResolutionMipmap reads the low resolution texture information // This is normally what you see previewed in Hammer texture browser. // The largest axis should always be 16 wide/tall. The smallest can be any value, // but is padded out to divisible by 4 for Dxt1 compressionn reasons @@ -119,12 +143,12 @@ func (reader *Reader) readLowResolutionMipmap(header *Header, buffer []byte) ([] return imgBuffer, nil } -// readMipmaps: Read all mipmaps +// readMipmaps reads all mipmaps // Returned format is a bit odd, but is just a set of flat arrays containing arrays: // mipmap[frame[face[slice[RGBA]]] func (reader *Reader) readMipmaps(header *Header, buffer []byte) ([][][][][]byte, error) { if header.Depth > 1 { - return [][][][][]byte{}, errors.New("only vtf textures with depth 1 are supported") + return [][][][][]byte{}, ErrorTextureDepthNotSupported } depth := header.Depth @@ -160,7 +184,7 @@ func (reader *Reader) readMipmaps(header *Header, buffer []byte) ([][][][][]byte mipmapSizes[mipmapIdx][1], storedFormat) if len(buffer) < bufferOffset+bufferSize { - return mipMaps, errors.New("expected data size is smaller than actual") + return mipMaps, ErrorMipmapSizeMismatch } img := buffer[bufferOffset : bufferOffset+bufferSize] From 5bb6ae5df894f2b4802f969e55800887c7c85357 Mon Sep 17 00:00:00 2001 From: Galaco Date: Sat, 26 Oct 2019 13:06:12 +0900 Subject: [PATCH 2/2] Fix mipmap rare case of an 8byte OOB --- reader.go | 34 ++++++++++++++++------------------ vtf.go | 13 +++++++++---- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/reader.go b/reader.go index c74946d..c0e2ded 100644 --- a/reader.go +++ b/reader.go @@ -10,7 +10,6 @@ import ( ) const ( - headerSize = 96 vtfSignature = "VTF\x00" ) @@ -69,7 +68,7 @@ func (reader *Reader) Read() (*Vtf, error) { } // Mipmaps - highResImage, err := reader.readMipmaps(header, buf.Bytes()[header.HeaderSize+uint32(len(lowResImage)):]) + highResImage, err := reader.readMipmaps(header, buf.Bytes()) if err != nil { return nil, err } @@ -84,10 +83,9 @@ func (reader *Reader) Read() (*Vtf, error) { // parseHeader reads vtf Header. func (reader *Reader) parseHeader(buffer []byte) (*Header, error) { - - // We know Header is 96 bytes + // We know Header is 96 bytes maximum // Note it *may* not be someday... - headerBytes := make([]byte, headerSize) + headerBytes := make([]byte, 96) // Read bytes equal to Header size byteReader := bytes.NewReader(buffer) @@ -133,7 +131,7 @@ func (reader *Reader) readLowResolutionMipmap(header *Header, buffer []byte) ([] format.Dxt1) imgBuffer := make([]byte, bufferSize) - byteReader := bytes.NewReader(buffer[headerSize : headerSize+bufferSize]) + byteReader := bytes.NewReader(buffer[int(header.HeaderSize) : int(header.HeaderSize)+bufferSize]) sectionReader := io.NewSectionReader(byteReader, 0, int64(bufferSize)) _, err := sectionReader.Read(imgBuffer) if err != nil { @@ -146,36 +144,36 @@ func (reader *Reader) readLowResolutionMipmap(header *Header, buffer []byte) ([] // readMipmaps reads all mipmaps // Returned format is a bit odd, but is just a set of flat arrays containing arrays: // mipmap[frame[face[slice[RGBA]]] -func (reader *Reader) readMipmaps(header *Header, buffer []byte) ([][][][][]byte, error) { +func (reader *Reader) readMipmaps(header *Header, buffer []byte) ([][][][][]uint8, error) { if header.Depth > 1 { - return [][][][][]byte{}, ErrorTextureDepthNotSupported + return [][][][][]uint8{}, ErrorTextureDepthNotSupported } depth := header.Depth - // This shouldn't ever happen, yet it occasionally seems to + // Occurs in texture versions before 7.2 if depth == 0 { depth = 1 } // Only support 1 ZSlice. No known Source game can use > 1 zslices numZSlice := uint16(1) - bufferOffset := 0 + bufferEnd := len(buffer) storedFormat := format.Format(header.HighResImageFormat) mipmapSizes := internal.ComputeMipmapSizes(int(header.MipmapCount), int(header.Width), int(header.Height)) // Iterate mipmap; smallest to largest - mipMaps := make([][][][][]byte, header.MipmapCount) - for mipmapIdx := uint8(0); mipmapIdx < header.MipmapCount; mipmapIdx++ { + mipMaps := make([][][][][]uint8, header.MipmapCount) + for mipmapIdx := int8(header.MipmapCount - 1); mipmapIdx >= int8(0); mipmapIdx-- { // Frame by frame; first to last - frames := make([][][][]byte, header.Frames) + frames := make([][][][]uint8, header.Frames) for frameIdx := uint16(0); frameIdx < header.Frames; frameIdx++ { - faces := make([][][]byte, 1) + faces := make([][][]uint8, 1) // Face by face; first to last // @TODO is this correct to use depth? How to know how many faces there are for faceIdx := uint16(0); faceIdx < depth; faceIdx++ { - zSlices := make([][]byte, 1) + zSlices := make([][]uint8, 1) // Z Slice by Z Slice; first to last // @TODO wtf is a z slice, and how do we know how many there are for sliceIdx := uint16(0); sliceIdx < numZSlice; sliceIdx++ { @@ -183,12 +181,12 @@ func (reader *Reader) readMipmaps(header *Header, buffer []byte) ([][][][][]byte mipmapSizes[mipmapIdx][0], mipmapSizes[mipmapIdx][1], storedFormat) - if len(buffer) < bufferOffset+bufferSize { + if len(buffer) < bufferEnd-bufferSize { return mipMaps, ErrorMipmapSizeMismatch } - img := buffer[bufferOffset : bufferOffset+bufferSize] + img := buffer[bufferEnd-bufferSize : bufferEnd] - bufferOffset += bufferSize + bufferEnd -= bufferSize zSlices[sliceIdx] = img } faces[faceIdx] = zSlices diff --git a/vtf.go b/vtf.go index 78fd0eb..5de9c00 100644 --- a/vtf.go +++ b/vtf.go @@ -6,7 +6,7 @@ type Vtf struct { header Header resources []byte lowResolutionImageData []uint8 - highResolutionImageData [][][][][]byte //[]mipmap[]frame[]face[]slice + highResolutionImageData [][][][][]uint8 //[]mipmap[]frame[]face[]slice } // Header returns vtf Header @@ -20,13 +20,18 @@ func (vtf *Vtf) LowResImageData() []uint8 { } // HighResImageData returns all data for all mipmaps -func (vtf *Vtf) HighResImageData() [][][][][]byte { +func (vtf *Vtf) HighResImageData() [][][][][]uint8 { return vtf.highResolutionImageData } +// Image returns raw data of the first frame of the highest resolution mipmap +func (vtf *Vtf) Image() []uint8 { + return vtf.HighestResolutionImageForFrame(0) +} + // MipmapsForFrame returns all mipmap sizes for a single frame -func (vtf *Vtf) MipmapsForFrame(frame int) [][]byte { - ret := make([][]byte, vtf.header.MipmapCount) +func (vtf *Vtf) MipmapsForFrame(frame int) [][]uint8 { + ret := make([][]uint8, vtf.header.MipmapCount) for idx, mipmap := range vtf.highResolutionImageData { ret[idx] = mipmap[frame][0][0]