package car import ( "bytes" "compress/flate" "compress/gzip" "compress/zlib" "encoding/binary" "fmt" "image" "image/color" "io" "os" "path/filepath" "strings" "github.com/apex/log" "github.com/blacktop/go-macho/types" "github.com/blacktop/ipsw/internal/magic" "github.com/blacktop/ipsw/pkg/comp" "github.com/blacktop/lzfse-cgo" ) const ( PixFmtARGB = "ARGB" // Color image PixFmtARGB16 = "RGBW" // Deep color image PixFmtRGB555 = "RGB5" // Packed 16-bit per pixel opaque image PixFmtGray = "GA8 " // Gray scale image with alpha PixFmtGray16 = "GA16" // Deep gray scale image with alpha PixFmtPDF = "PDF " // PDF raw bytes PixFmtJPEG = "JPEG" // JPEG raw bytes PixFmtHEIF = "HEIF" // HEIF raw bytes PixFmtRawData = "DATA" // Raw bytes ) type compressionType uint32 const ( Uncompressed compressionType = 0 RLE compressionType = 1 ZIP compressionType = 2 LZVN compressionType = 3 LZFSE compressionType = 4 JPEGLZFSE compressionType = 5 BlurredImage compressionType = 6 ASTCImage compressionType = 7 PaletteImage compressionType = 8 HEVC compressionType = 9 DeepmapLZFSE compressionType = 10 Deepmap2 compressionType = 11 DXTC compressionType = 12 ) type csiBitmapFlags uint32 func (f csiBitmapFlags) ChunksFollow() bool { return types.ExtractBits(uint64(f), 0, 1) == 1 } func (f csiBitmapFlags) IsOpaque() bool { return types.ExtractBits(uint64(f), 1, 1) == 1 } func (f csiBitmapFlags) String() string { return fmt.Sprintf("chunks_follow: %t, is_opaque: %t", f.ChunksFollow(), f.IsOpaque()) } type csiBitmap struct { Signature [4]byte // 'PELM' Flags csiBitmapFlags Encoding compressionType Length uint32 // Data []byte } type csiBitmapChunk struct { Signature [4]byte // 'PECH' PELM Chunk Flags uint32 // always 0 Version uint32 Rows uint32 Length uint32 // Data []byte } type csiRawData struct { Signature [4]byte // 'RAWD' Flags uint32 Length uint32 // Data []byte } type csiJpegLZFSEData struct { Version uint32 ChucksFollowing uint32 LzfseAlphaSize uint32 LzfseDataRowBytes uint32 JpegDataSize uint32 } type csiASTCData struct { Version uint32 // 0 == raw ATSC Data; 1 == lzfse compressed ATSC Data DataSize uint32 AstcDataSize uint32 // Data []byte } type csiHEVCData struct { Version uint32 HevcDataSize uint32 } type csiDeepmapData struct { Version uint32 PixelFormat uint32 CompressedBytes uint64 } type csiDeepmap2Data struct { Version uint32 Encoding compressionType Length uint64 } type deepmapPixelFormat uint8 const ( ImageDeepmapPixelFormatG8 deepmapPixelFormat = 0x01 ImageDeepmapPixelFormatGA8 deepmapPixelFormat = 0x02 ImageDeepmapPixelFormatRGB8 deepmapPixelFormat = 0x03 ImageDeepmapPixelFormatRGBA8 deepmapPixelFormat = 0x04 ImageDeepmapPixelFormatRGBA16 deepmapPixelFormat = 0x0A // 16-bit per channel RGBA ImageDeepmapPixelFormatG16F deepmapPixelFormat = 0x11 ImageDeepmapPixelFormatGA16F deepmapPixelFormat = 0x12 ImageDeepmapPixelFormatRGB16F deepmapPixelFormat = 0x13 ImageDeepmapPixelFormatRGBA16F deepmapPixelFormat = 0x14 ) type deepmapCompressionMethod uint8 const ( ImageDeepmapCompressionNone deepmapCompressionMethod = 1 ImageDeepmapCompressionDefault deepmapCompressionMethod = 2 ImageDeepmapCompressionLossless deepmapCompressionMethod = 3 ImageDeepmapCompressionPalette deepmapCompressionMethod = 4 ) type deepmap struct { Signature [4]byte // 'dmap' CompressionMethod deepmapCompressionMethod Scale uint8 Unknown uint8 // 10 ? PixelFormat deepmapPixelFormat CompressedBlock uint32 } type deepmap2 struct { Signature [4]byte // 'dmp2' Scale uint8 BlobVersion uint8 // 1 PixelFormat deepmapPixelFormat CompressionMethod deepmapCompressionMethod Width uint16 Height uint16 CompressedBlock uint32 } // BGRA to RGBA type BGRA struct { image.RGBA } func (p *BGRA) RGBAAt(x, y int) color.RGBA { c := p.RGBA.RGBAAt(x, y) return color.RGBA{R: c.B, G: c.G, B: c.R, A: c.A} } func (p *BGRA) At(x, y int) color.Color { return p.RGBAAt(x, y) } func (p *BGRA) SubImage(r image.Rectangle) image.Image { c := p.RGBA.SubImage(r).(*image.RGBA) return &BGRA{*c} } type GA8 struct { Pix []uint8 Stride int Rect image.Rectangle } func (p *GA8) ColorModel() color.Model { return color.RGBAModel } func (p *GA8) Bounds() image.Rectangle { return p.Rect } func (p *GA8) At(x, y int) color.Color { return p.GA8At(x, y) } func (p *GA8) GA8At(x, y int) color.RGBA { if !(image.Point{x, y}.In(p.Rect)) { return color.RGBA{} } i := p.PixOffset(x, y) if i+2 > len(p.Pix) { return color.RGBA{} } s := p.Pix[i : i+2 : i+2] // Small cap improves performance, see https://golang.org/issue/27857 return color.RGBA{s[0], s[0], s[0], s[1]} } func (p *GA8) PixOffset(x, y int) int { return (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*2 } func unsupportedJPEGCompression() error { return fmt.Errorf("unsupported JPEGLZFSE decode: JPEG color data with optional LZFSE alpha is not implemented") } func decodeImage(r io.Reader, ci csiHeader, conf *Config, rowBytesOverride int) (image.Image, error) { var out bytes.Buffer // Track Deepmap2 origin and pixel format so we can render without BGRA swap fromDeepmap2 := false var deepmap2PixFmt deepmapPixelFormat var elem csiBitmap if err := binary.Read(r, binary.LittleEndian, &elem); err != nil { return nil, fmt.Errorf("failed to read CSIBitmap: %s", err) } // log.WithFields(log.Fields{ // "signature": string(elem.Signature[:]), // "flags": elem.Flags.String(), // "encoding": elem.Encoding, // "length": elem.Length, // }).Info("Reading CSIElement") if elem.Flags.ChunksFollow() { for i := uint32(0); i < elem.Length; i++ { var chunk csiBitmapChunk if err := binary.Read(r, binary.LittleEndian, &chunk); err != nil { return nil, err } if chunk.Signature != [4]byte{'K', 'C', 'B', 'C'} { return nil, fmt.Errorf("invalid chunk signature: %s", chunk.Signature) } data := make([]byte, chunk.Length) if err := binary.Read(r, binary.LittleEndian, &data); err != nil { return nil, err } switch elem.Encoding { case Uncompressed: out.Write(data) case RLE: out.Write(decodeRLE(data)) case ZIP: // Try gzip first gr, err := gzip.NewReader(bytes.NewReader(data)) if err == nil { if _, err := io.Copy(&out, gr); err != nil { return nil, fmt.Errorf("failed to decompress gzip: %v", err) } } else { // Try zlib zr, err := zlib.NewReader(bytes.NewReader(data)) if err == nil { if _, err := io.Copy(&out, zr); err != nil { return nil, fmt.Errorf("failed to decompress zlib: %v", err) } zr.Close() } else { // Try raw deflate fr := flate.NewReader(bytes.NewReader(data)) if _, err := io.Copy(&out, fr); err != nil { // Last resort - assume uncompressed out.Write(data) } fr.Close() } } case LZVN: dec := make([]byte, len(data)*4) if n := lzfse.DecodeLZVNBuffer(data, dec); n == 0 { return nil, fmt.Errorf("failed to decompress lzvn data") } else { out.Write(dec[:n]) } case LZFSE: decompressed, err := comp.Decompress(data, comp.LZFSE) if err != nil { return nil, fmt.Errorf("failed to decompress LZFSE data: %v", err) } out.Write(decompressed) case JPEGLZFSE: return nil, unsupportedJPEGCompression() case HEVC: // HEVC/H.265 video data var hevcInfo csiHEVCData r := bytes.NewReader(data) if err := binary.Read(r, binary.LittleEndian, &hevcInfo); err != nil { return nil, fmt.Errorf("failed to read HEVC header: %v", err) } // Read the HEVC data hevcData := make([]byte, hevcInfo.HevcDataSize) if err := binary.Read(r, binary.LittleEndian, &hevcData); err != nil { return nil, fmt.Errorf("failed to read HEVC data: %v", err) } // Output raw HEVC data (would need HEVC decoder for actual image) out.Write(hevcData) case PaletteImage: // Magic byte detection for compression format if len(data) >= 4 { magic := data[0:4] if string(magic) == "lzvn" || (len(data) >= 4 && magic[0] == 0x68 && magic[1] == 0x01 && magic[2] == 0x00 && magic[3] == 0xf0) { // LZVN compressed palette image dec := make([]byte, len(data)*4) if n := lzfse.DecodeLZVNBuffer(data, dec); n == 0 { return nil, fmt.Errorf("failed to decompress PaletteImage LZVN data") } else { out.Write(dec[:n]) } } else { // Raw palette image data out.Write(data) } } else { out.Write(data) } case ASTCImage: // ASTC texture compression var astcInfo csiASTCData r := bytes.NewReader(data) if err := binary.Read(r, binary.LittleEndian, &astcInfo); err != nil { return nil, fmt.Errorf("failed to read ASTC header: %v", err) } // Read the ASTC data astcData := make([]byte, astcInfo.AstcDataSize) if err := binary.Read(r, binary.LittleEndian, &astcData); err != nil { return nil, fmt.Errorf("failed to read ASTC data: %v", err) } if astcInfo.Version == 1 { // LZFSE compressed ASTC data decompressed, err := comp.Decompress(astcData, comp.LZFSE) if err != nil { return nil, fmt.Errorf("failed to decompress ASTC data: %v", err) } out.Write(decompressed) } else { // Raw ASTC data out.Write(astcData) } case DeepmapLZFSE: // Deepmap with LZFSE compression var deepmapInfo csiDeepmapData r := bytes.NewReader(data) if err := binary.Read(r, binary.LittleEndian, &deepmapInfo); err != nil { return nil, fmt.Errorf("failed to read Deepmap header: %v", err) } log.WithFields(log.Fields{ "version": deepmapInfo.Version, "pixel_format": deepmapInfo.PixelFormat, "compressed_bytes": deepmapInfo.CompressedBytes, }).Debug("Reading Deepmap") // Read the deepmap structure header var dm deepmap if err := binary.Read(r, binary.LittleEndian, &dm); err != nil { return nil, fmt.Errorf("failed to read deepmap structure: %v", err) } if dm.Signature != [4]byte{'d', 'm', 'a', 'p'} { return nil, fmt.Errorf("invalid deepmap signature: %s", dm.Signature) } // Read the compressed data compressedData := make([]byte, dm.CompressedBlock) if err := binary.Read(r, binary.LittleEndian, &compressedData); err != nil { return nil, fmt.Errorf("failed to read Deepmap compressed data: %v", err) } log.WithFields(log.Fields{ "signature": string(dm.Signature[:]), "compression_method": dm.CompressionMethod, "scale": dm.Scale, "pixel_format": dm.PixelFormat, "compressed_block": dm.CompressedBlock, }).Debug("Reading Deepmap") if isLZFSE, _ := magic.IsLZFSE(compressedData); isLZFSE { // Decompress the data decompressed, err := comp.Decompress(compressedData, comp.LZFSE) if err != nil { return nil, fmt.Errorf("failed to decompress DeepmapLZFSE data: %v", err) } out.Write(decompressed) } else { out.Write(compressedData) } // switch dm.CompressionMethod { // case ImageDeepmapCompressionDefault: // // Decompress the data // decompressed, err := comp.Decompress(compressedData, comp.LZFSE) // if err != nil { // return nil, fmt.Errorf("failed to decompress DeepmapLZFSE data: %v", err) // } // out.Write(decompressed) // case ImageDeepmapCompressionNone: // out.Write(compressedData) // default: // out.Write(compressedData) // // return nil, fmt.Errorf("unsupported deepmap compression method: %d", dm.CompressionMethod) // } case Deepmap2: // Handle Deepmap2 inside chunk stream rdm := bytes.NewReader(data) var cdm2 csiDeepmap2Data if err := binary.Read(rdm, binary.LittleEndian, &cdm2); err != nil { return nil, err } var dm2 deepmap2 if err := binary.Read(rdm, binary.LittleEndian, &dm2); err != nil { return nil, err } if dm2.Signature != [4]byte{'d', 'm', 'p', '2'} { return nil, fmt.Errorf("invalid deepmap2 signature: %s", dm2.Signature) } // Override dimensions; keep CSI pixel format (authoritative for GA8 masks) ci.Width = uint32(dm2.Width) ci.Height = uint32(dm2.Height) fromDeepmap2 = true deepmap2PixFmt = dm2.PixelFormat // Gather compressed payload which may span multiple KCBC chunks need := int(dm2.CompressedBlock) compressed := make([]byte, need) readSoFar := 0 if rdm.Len() > 0 { frag := make([]byte, rdm.Len()) if _, err := io.ReadFull(rdm, frag); err == nil { toCopy := min(len(frag), need) copy(compressed[0:toCopy], frag[:toCopy]) readSoFar += toCopy } } // If not enough, consume following KCBC chunks from the outer reader 'r'. // Cap to remaining chunks in the element to avoid reading past the end. extraChunks := uint32(0) maxExtra := elem.Length - 1 - i for readSoFar < int(dm2.CompressedBlock) && extraChunks < maxExtra { var next csiBitmapChunk if err := binary.Read(r, binary.LittleEndian, &next); err != nil { break } if next.Signature != [4]byte{'K', 'C', 'B', 'C'} { break } buf := make([]byte, next.Length) if err := binary.Read(r, binary.LittleEndian, &buf); err != nil { break } remain := int(dm2.CompressedBlock) - readSoFar toCopy := min(len(buf), remain) copy(compressed[readSoFar:readSoFar+toCopy], buf[:toCopy]) readSoFar += toCopy extraChunks++ } // Skip accounting for consumed chunks in the outer loop i += extraChunks compressed = compressed[:readSoFar] // Palette compression carries a palette before an LZFSE block; decode directly to image if dm2.CompressionMethod == ImageDeepmapCompressionPalette { magic := []byte("bvx2") pos := bytes.Index(compressed, magic) if pos > 0 { palette := compressed[:pos] indicesCompressed := compressed[pos:] decomp, err := comp.Decompress(indicesCompressed, comp.LZFSE) if err == nil { // Compose [palette][indices] and decode combo := append([]byte{}, append(palette, decomp...)...) if palImg, err := decodePalettedImage(combo, int(ci.Width), int(ci.Height)); err == nil { return palImg, nil } } } } switch cdm2.Encoding { case LZFSE: if isLZFSE, _ := magic.IsLZFSE(compressed); isLZFSE { decompressed, err := comp.Decompress(compressed, comp.LZFSE) if err != nil { return nil, fmt.Errorf("failed to decompress Deepmap2 LZFSE data: %v", err) } out.Write(decompressed) } else { // Some payloads are raw when marked LZFSE out.Write(compressed) } case ZIP: if isLZFSE, _ := magic.IsLZFSE(compressed); isLZFSE { decompressed, err := comp.Decompress(compressed, comp.LZFSE) if err != nil { return nil, fmt.Errorf("failed to decompress Deepmap2 LZFSE-as-ZIP data: %v", err) } out.Write(decompressed) break } if gr, err := gzip.NewReader(bytes.NewReader(compressed)); err == nil { if _, err := io.Copy(&out, gr); err != nil { return nil, fmt.Errorf("failed to decompress gzip data: %v", err) } } else if zr, err := zlib.NewReader(bytes.NewReader(compressed)); err == nil { if _, err := io.Copy(&out, zr); err != nil { return nil, fmt.Errorf("failed to decompress zlib data: %v", err) } zr.Close() } else { fr := flate.NewReader(bytes.NewReader(compressed)) if _, err := io.Copy(&out, fr); err != nil { // Assume raw on failure out.Write(compressed) } fr.Close() } case Deepmap2: // Nested Deepmap2 often uses LZFSE after a small header if len(data) > 16 { decompressed, err := comp.Decompress(data[16:], comp.LZFSE) if err != nil { return nil, fmt.Errorf("failed to decompress nested Deepmap2: %v", err) } out.Write(decompressed) } else { out.Write(compressed) } default: return nil, fmt.Errorf("unsupported deepmap2 encoding: %s", cdm2.Encoding) } default: return nil, fmt.Errorf("unknown encoding: %s (value: %d)", elem.Encoding, elem.Encoding) } } } else { data := make([]byte, elem.Length) if err := binary.Read(r, binary.LittleEndian, &data); err != nil { return nil, err } switch elem.Encoding { case Uncompressed: out.Write(data) case RLE: out.Write(decodeRLE(data)) case ZIP: // Try gzip first gr, err := gzip.NewReader(bytes.NewReader(data)) if err == nil { if _, err := io.Copy(&out, gr); err != nil { return nil, fmt.Errorf("failed to decompress gzip: %v", err) } } else { // Try zlib zr, err := zlib.NewReader(bytes.NewReader(data)) if err == nil { if _, err := io.Copy(&out, zr); err != nil { return nil, fmt.Errorf("failed to decompress zlib: %v", err) } zr.Close() } else { // Try raw deflate fr := flate.NewReader(bytes.NewReader(data)) if _, err := io.Copy(&out, fr); err != nil { // Last resort - assume uncompressed out.Write(data) } fr.Close() } } case LZVN: dec := make([]byte, len(data)*4) if n := lzfse.DecodeLZVNBuffer(data, dec); n == 0 { return nil, fmt.Errorf("failed to decompress lzvn data") } else { out.Write(dec[:n]) } case LZFSE: decompressed, err := comp.Decompress(data, comp.LZFSE) if err != nil { return nil, fmt.Errorf("failed to decompress LZFSE data: %v", err) } out.Write(decompressed) case JPEGLZFSE: return nil, unsupportedJPEGCompression() case HEVC: // HEVC/H.265 video data var hevcInfo csiHEVCData r := bytes.NewReader(data) if err := binary.Read(r, binary.LittleEndian, &hevcInfo); err != nil { return nil, fmt.Errorf("failed to read HEVC header: %v", err) } // Read the HEVC data hevcData := make([]byte, hevcInfo.HevcDataSize) if err := binary.Read(r, binary.LittleEndian, &hevcData); err != nil { return nil, fmt.Errorf("failed to read HEVC data: %v", err) } // Output raw HEVC data (would need HEVC decoder for actual image) out.Write(hevcData) case ASTCImage: // ASTC texture compression var astcInfo csiASTCData r := bytes.NewReader(data) if err := binary.Read(r, binary.LittleEndian, &astcInfo); err != nil { return nil, fmt.Errorf("failed to read ASTC header: %v", err) } // Read the ASTC data astcData := make([]byte, astcInfo.AstcDataSize) if err := binary.Read(r, binary.LittleEndian, &astcData); err != nil { return nil, fmt.Errorf("failed to read ASTC data: %v", err) } if astcInfo.Version == 1 { // LZFSE compressed ASTC data decompressed, err := comp.Decompress(astcData, comp.LZFSE) if err != nil { return nil, fmt.Errorf("failed to decompress ASTC data: %v", err) } out.Write(decompressed) } else { // Raw ASTC data out.Write(astcData) } case PaletteImage: if isLZFSE, _ := magic.IsLZFSE(data); isLZFSE { // Decompress the data decompressed, err := comp.Decompress(data, comp.LZFSE) if err != nil { return nil, fmt.Errorf("failed to decompress DeepmapLZFSE data: %v", err) } out.Write(decompressed) } else { // Raw palette image data out.Write(data) } // After decompression, the data should contain palette indices // We'll handle this in the pixel format switch below case DeepmapLZFSE: // Deepmap with LZFSE compression var deepmapInfo csiDeepmapData r := bytes.NewReader(data) if err := binary.Read(r, binary.LittleEndian, &deepmapInfo); err != nil { return nil, fmt.Errorf("failed to read Deepmap header: %v", err) } var dm deepmap if err := binary.Read(r, binary.LittleEndian, &dm); err != nil { return nil, err } if dm.Signature != [4]byte{'d', 'm', 'a', 'p'} { return nil, fmt.Errorf("invalid deepmap signature: %s", dm.Signature) } // Read the compressed data compressedData := make([]byte, dm.CompressedBlock) if err := binary.Read(r, binary.LittleEndian, &compressedData); err != nil { return nil, fmt.Errorf("failed to read Deepmap compressed data: %v", err) } switch dm.CompressionMethod { case ImageDeepmapCompressionLossless: if isLZFSE, _ := magic.IsLZFSE(compressedData); isLZFSE { // Decompress the data decompressed, err := comp.Decompress(compressedData, comp.LZFSE) if err != nil { return nil, fmt.Errorf("failed to decompress DeepmapLZFSE data: %v", err) } out.Write(decompressed) } else { return nil, fmt.Errorf("lzfse magic bytes not found in DeepmapLZFSE data") } case ImageDeepmapCompressionDefault: out.Write(decodeRLE(compressedData)) case ImageDeepmapCompressionNone: out.Write(compressedData) case ImageDeepmapCompressionPalette: return nil, fmt.Errorf("deepmap palette compression not implemented") } case Deepmap2: dmr := bytes.NewReader(data) var cdm2 csiDeepmap2Data if err := binary.Read(dmr, binary.LittleEndian, &cdm2); err != nil { return nil, err } if conf != nil && conf.Verbose { log.WithFields(log.Fields{ "version": cdm2.Version, "encoding": cdm2.Encoding, "length": cdm2.Length, }).Info("Reading Deepmap2 Data") } var dm2 deepmap2 if err := binary.Read(dmr, binary.LittleEndian, &dm2); err != nil { return nil, err } if dm2.Signature != [4]byte{'d', 'm', 'p', '2'} { return nil, fmt.Errorf("invalid deepmap2 signature: %s", dm2.Signature) } if conf != nil && conf.Verbose { log.WithFields(log.Fields{ "signature": string(dm2.Signature[:]), "blob_version": dm2.BlobVersion, "pixel_format": dm2.PixelFormat, "compression_method": dm2.CompressionMethod, "width": dm2.Width, "height": dm2.Height, "scale": dm2.Scale, "compressed_block": dm2.CompressedBlock, }).Warn("Reading Deepmap2") } if dm2.BlobVersion != 1 { return nil, fmt.Errorf("unsupported deepmap2 blob version: %d", dm2.BlobVersion) } // Override dimensions; keep CSI pixel format (authoritative for GA8 masks) ci.Width = uint32(dm2.Width) ci.Height = uint32(dm2.Height) fromDeepmap2 = true deepmap2PixFmt = dm2.PixelFormat switch cdm2.Encoding { case LZFSE, ZIP: // Gather the full Deepmap2 compressed block, which may follow in KCBC chunks need := int(dm2.CompressedBlock) compressed := make([]byte, need) readSoFar := 0 if dmr.Len() > 0 { frag := make([]byte, dmr.Len()) if _, err := io.ReadFull(dmr, frag); err == nil { toCopy := min(len(frag), need) copy(compressed[0:], frag[:toCopy]) readSoFar += toCopy } } // In the non-chunked path, the blob may be smaller than // CompressedBlock claims — proceed with what we have. compressed = compressed[:readSoFar] // If palette compression, split palette and indices and decode paletted directly if dm2.CompressionMethod == ImageDeepmapCompressionPalette { // Look for LZFSE block header magic commonly seen as 'bvx2' pos := bytes.Index(compressed, []byte("bvx2")) if pos > 0 { palette := compressed[:pos] indicesCompressed := compressed[pos:] // Some files mark ZIP but actually contain LZFSE decomp, err := comp.Decompress(indicesCompressed, comp.LZFSE) if err == nil { combo := append([]byte{}, append(palette, decomp...)...) if palImg, err := decodePalettedImage(combo, int(ci.Width), int(ci.Height)); err == nil { return palImg, nil } } } } // Otherwise, decompress the full block if cdm2.Encoding == ZIP { if isLZFSE, _ := magic.IsLZFSE(compressed); isLZFSE { decompressed, err := comp.Decompress(compressed, comp.LZFSE) if err != nil { return nil, fmt.Errorf("failed to decompress Deepmap2 LZFSE data: %v", err) } out.Write(decompressed) } else if gr, err := gzip.NewReader(bytes.NewReader(compressed)); err == nil { if _, err := io.Copy(&out, gr); err != nil { return nil, fmt.Errorf("failed to decompress gzip data: %v", err) } } else if zr, err := zlib.NewReader(bytes.NewReader(compressed)); err == nil { if _, err := io.Copy(&out, zr); err != nil { return nil, fmt.Errorf("failed to decompress zlib data: %v", err) } zr.Close() } else { fr := flate.NewReader(bytes.NewReader(compressed)) if _, err := io.Copy(&out, fr); err != nil { out.Write(compressed) } fr.Close() } } else { if isLZFSE, _ := magic.IsLZFSE(compressed); isLZFSE { decompressed, err := comp.Decompress(compressed, comp.LZFSE) if err != nil { return nil, fmt.Errorf("failed to decompress LZFSE data: %v", err) } out.Write(decompressed) } else { out.Write(compressed) } } case Deepmap2: // Handle nested Deepmap2 encoding if dmr.Len() > 16 { payload := make([]byte, dmr.Len()) if _, err := io.ReadFull(dmr, payload); err == nil { decompressed, err := comp.Decompress(payload[16:], comp.LZFSE) if err != nil { return nil, fmt.Errorf("failed to decompress nested Deepmap2: %v", err) } out.Write(decompressed) } } default: return nil, fmt.Errorf("unsupported deepmap2 encoding: %s", cdm2.Encoding) } default: return nil, fmt.Errorf("unknown encoding: %s (value: %d)", elem.Encoding, elem.Encoding) } } // Check for invalid image dimensions if ci.Width == 0 || ci.Height == 0 { return nil, fmt.Errorf("invalid image dimensions: %dx%d", ci.Width, ci.Height) } // Check if this was a PaletteImage compression type // If so, decode as a paletted image regardless of pixel format if elem.Encoding == PaletteImage { // Try to decode as a paletted image if palImg, err := decodePalettedImage(out.Bytes(), int(ci.Width), int(ci.Height)); err == nil { return palImg, nil } else { // If palette decoding fails, log and try normal pixel format handling as fallback log.Debugf("Failed to decode as PaletteImage for %s: %v, falling back to pixel format", string(bytes.Trim(ci.Metadata.Name[:], "\x00")), err) } } format := string(ci.PixelFormat[:]) switch format { case PixFmtARGB, PixFmtARGB16: // Special handling for IconImage layout which often uses channel-separated ARGB format (AAAA RRRR GGGG BBBB) if ci.Metadata.Layout == IconImage { pixelCount := int(ci.Width * ci.Height) // Prefer exact-sized channel data, but allow >= to tolerate minor padding if out.Len() >= pixelCount*4 { if img, err := decodeAppIconARGB(out.Bytes(), int(ci.Width), int(ci.Height)); err == nil { return img, nil } } } var offset int bytesPerPixel := 4 // Default for ARGB (8-bit per channel) if format == PixFmtARGB16 { if !fromDeepmap2 { return nil, fmt.Errorf("unsupported pixel format: %s outside Deepmap2", format) } bytesPerPixel = 8 // 16-bit per channel (2 bytes per channel × 4 channels) } expectedSize := int(ci.Width * ci.Height * uint32(bytesPerPixel)) actualSize := out.Len() if actualSize < expectedSize { // Dump raw bytes for debugging when verbose if conf != nil && conf.Verbose && conf.Export && conf.Output != "" { name := strings.Trim(string(bytes.Trim(ci.Metadata.Name[:], "\x00")), " ") if name == "" { name = fmt.Sprintf("asset_%dx%d_%s", ci.Width, ci.Height, format) } errPath := filepath.Join(conf.Output, fmt.Sprintf("%s.error", name)) _ = os.WriteFile(errPath, out.Bytes(), 0644) } return nil, fmt.Errorf("insufficient image data: got %d bytes, expected %d bytes for %dx%d %s", actualSize, expectedSize, ci.Width, ci.Height, format) } if v := actualSize - expectedSize; v != 0 { offset = v / int(ci.Height*uint32(bytesPerPixel)) } rect := image.Rectangle{ Min: image.Point{0, 0}, Max: image.Point{ X: int(ci.Width), Y: int(ci.Height), }, } // rgba := image.NewRGBA(rect) // rgba.Pix = out.Bytes() // rgba.Stride = (rect.Dx() + offset) * 4 // return rgba, nil // Deepmap2 emits RGBA ordering; do not apply BGRA swap, and downconvert 16-bit to 8-bit if fromDeepmap2 { if format == PixFmtARGB16 || deepmap2PixFmt == ImageDeepmapPixelFormatRGBA16 { // downconvert RGBA16 (LE) to RGBA8 in := out.Bytes() px := rect.Dx() * rect.Dy() dst := make([]byte, px*4) for i := range px { j := i * 8 // take high byte of each 16-bit LE component r := in[j+1] g := in[j+3] b := in[j+5] a := in[j+7] k := i * 4 dst[k+0] = r dst[k+1] = g dst[k+2] = b dst[k+3] = a } stride := rect.Dx() * 4 img := &image.RGBA{Pix: dst, Stride: stride, Rect: rect} return img, nil } // RGBA8 stride := rect.Dx() * 4 if rowBytesOverride > 0 { stride = rowBytesOverride } img := &image.RGBA{Pix: out.Bytes(), Stride: stride, Rect: rect} return img, nil } // Default path: treat as BGRA in memory and swap when reading stride := (rect.Dx() + offset) * bytesPerPixel if rowBytesOverride > 0 { stride = rowBytesOverride } bgra := &BGRA{image.RGBA{ Pix: out.Bytes(), Stride: stride, Rect: rect, }} return bgra, nil case PixFmtGray, PixFmtGray16: if format == PixFmtGray16 { return nil, fmt.Errorf("unsupported pixel format: %s", format) } var offset int bytesPerPixel := 2 // GA8: gray + alpha (1 byte each) expectedSize := int(ci.Width * ci.Height * uint32(bytesPerPixel)) actualSize := out.Len() if actualSize < expectedSize { // Not enough data for the image return nil, fmt.Errorf("insufficient image data: got %d bytes, expected %d bytes for %dx%d %s", actualSize, expectedSize, ci.Width, ci.Height, format) } if v := actualSize - expectedSize; v != 0 { offset = v / int(ci.Height*uint32(bytesPerPixel)) } rect := image.Rectangle{ Min: image.Point{0, 0}, Max: image.Point{ X: int(ci.Width), Y: int(ci.Height), }, } stride := (rect.Dx() + offset) * bytesPerPixel if rowBytesOverride > 0 { stride = rowBytesOverride } bgra := &GA8{ Pix: out.Bytes(), Stride: stride, Rect: rect, } return bgra, nil default: return nil, fmt.Errorf("unknown pixel format: %s", format) } } // decodePalettedImage creates a paletted image from palette data and indices // The format is typically: [palette colors (256 * 4 bytes RGBA)][pixel indices] // cafef00dMagic identifies CoreUI palette rendition data. const cafef00dMagic = 0xcafef00d func decodePalettedImage(data []byte, width, height int) (image.Image, error) { pixelCount := width * height // CoreUI palette format: [uint32 magic 0xcafef00d][uint32 version] // [uint16 palette_count][count*4 RGBA entries][width*height indices] if len(data) >= 10 { magic := binary.LittleEndian.Uint32(data[0:4]) if magic == cafef00dMagic { count := int(binary.LittleEndian.Uint16(data[8:10])) hdr := 10 palBytes := count * 4 if len(data) < hdr+palBytes+pixelCount { return nil, fmt.Errorf("cafef00d palette data too short: got %d, need %d (hdr=%d palette=%d indices=%d)", len(data), hdr+palBytes+pixelCount, hdr, palBytes, pixelCount) } palette := make(color.Palette, count) for i := range count { off := hdr + i*4 palette[i] = color.RGBA{ R: data[off], G: data[off+1], B: data[off+2], A: data[off+3], } } indices := data[hdr+palBytes:] rect := image.Rect(0, 0, width, height) img := image.NewPaletted(rect, palette) copy(img.Pix, indices[:pixelCount]) return img, nil } } // Fallback: fixed 256-entry palette + indices const fixedPaletteSize = 256 fixedPalBytes := fixedPaletteSize * 4 if len(data) >= fixedPalBytes+pixelCount { palette := make(color.Palette, fixedPaletteSize) for i := range fixedPaletteSize { off := i * 4 palette[i] = color.RGBA{ R: data[off], G: data[off+1], B: data[off+2], A: data[off+3], } } rect := image.Rect(0, 0, width, height) img := image.NewPaletted(rect, palette) copy(img.Pix, data[fixedPalBytes:fixedPalBytes+pixelCount]) return img, nil } // Fallback: indices only (grayscale palette) if len(data) >= pixelCount { palette := make(color.Palette, 256) for i := range 256 { palette[i] = color.RGBA{uint8(i), uint8(i), uint8(i), 255} } rect := image.Rect(0, 0, width, height) img := image.NewPaletted(rect, palette) copy(img.Pix, data[:pixelCount]) return img, nil } return nil, fmt.Errorf("insufficient data for paletted image: got %d bytes, need %d (width=%d height=%d)", len(data), pixelCount, width, height) } // decodeAppIconARGB decodes AppIcon ARGB format where channels are separated // Instead of interleaved ARGBARGBARGB, the data is stored as AAARRRGGGBBB func decodeAppIconARGB(data []byte, width, height int) (image.Image, error) { pixelCount := width * height expectedSize := pixelCount * 4 // 4 bytes per pixel (ARGB) if len(data) < expectedSize { return nil, fmt.Errorf("insufficient data for AppIcon ARGB: got %d bytes, expected %d", len(data), expectedSize) } rect := image.Rectangle{ Min: image.Point{0, 0}, Max: image.Point{X: width, Y: height}, } img := image.NewRGBA(rect) // Extract the separate channels // The format is: all alpha values, then all red, then all green, then all blue alphaChannel := data[0:pixelCount] redChannel := data[pixelCount : pixelCount*2] greenChannel := data[pixelCount*2 : pixelCount*3] blueChannel := data[pixelCount*3 : pixelCount*4] // Reconstruct interleaved RGBA pixels for i := range pixelCount { pixelIndex := i * 4 img.Pix[pixelIndex+0] = redChannel[i] // R img.Pix[pixelIndex+1] = greenChannel[i] // G img.Pix[pixelIndex+2] = blueChannel[i] // B img.Pix[pixelIndex+3] = alphaChannel[i] // A } return img, nil }