mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-13 22:29:01 -04:00
Cap the IFD queue at maxIFDs so a file packed with huge SubIFD counts can no longer grow it without bound, and walk the embedded JPEG by a bounded number of header segments so a preview whose SOF sits past 64KB of leading metadata is still found instead of being scanned only within a fixed byte window.
239 lines
6.9 KiB
Go
239 lines
6.9 KiB
Go
package preprocessor
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"io"
|
|
|
|
thumbnailerErrors "github.com/opencloud-eu/opencloud/services/thumbnails/pkg/errors"
|
|
)
|
|
|
|
// RawTiffDecoder serves the embedded JPEG preview of TIFF-based camera raw
|
|
// files (NEF, CR2, PEF, ARW, SR2, DNG) instead of decoding raw sensor data.
|
|
type RawTiffDecoder struct{}
|
|
|
|
// Convert extracts the largest renderable embedded JPEG preview and decodes it
|
|
func (RawTiffDecoder) Convert(r io.Reader) (any, error) {
|
|
data, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
preview, orientation, err := extractEmbeddedJPEG(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// the rotation lives in the container, the preview carries no EXIF
|
|
if orientation > 1 {
|
|
preview = spliceJPEGOrientation(preview, orientation)
|
|
}
|
|
converter := ForType("image/jpeg", nil)
|
|
if converter == nil {
|
|
return nil, thumbnailerErrors.ErrNoImageFromRawFile
|
|
}
|
|
return converter.Convert(bytes.NewReader(preview))
|
|
}
|
|
|
|
const (
|
|
tiffTagStripOffsets = 0x0111
|
|
tiffTagOrientation = 0x0112
|
|
tiffTagStripByteCounts = 0x0117
|
|
tiffTagSubIFDs = 0x014a
|
|
tiffTagJPEGOffset = 0x0201 // JPEGInterchangeFormat
|
|
tiffTagJPEGLength = 0x0202 // JPEGInterchangeFormatLength
|
|
tiffTypeShort = 3
|
|
tiffTypeLong = 4
|
|
// bound work on untrusted input: cap processed IFDs (and the queue) and
|
|
// the JPEG header segments walked before the SOF marker
|
|
maxIFDs = 64
|
|
maxJPEGSegments = 32
|
|
)
|
|
|
|
// extractEmbeddedJPEG walks the IFD chain incl. SubIFDs and returns the
|
|
// largest renderable embedded JPEG plus the container orientation.
|
|
func extractEmbeddedJPEG(data []byte) ([]byte, uint16, error) {
|
|
if len(data) < 8 {
|
|
return nil, 0, thumbnailerErrors.ErrNoImageFromRawFile
|
|
}
|
|
var order binary.ByteOrder
|
|
switch {
|
|
case data[0] == 'I' && data[1] == 'I':
|
|
order = binary.LittleEndian
|
|
case data[0] == 'M' && data[1] == 'M':
|
|
order = binary.BigEndian
|
|
default:
|
|
return nil, 0, thumbnailerErrors.ErrNoImageFromRawFile
|
|
}
|
|
if order.Uint16(data[2:4]) != 42 {
|
|
return nil, 0, thumbnailerErrors.ErrNoImageFromRawFile
|
|
}
|
|
|
|
type candidate struct{ offset, length uint32 }
|
|
var candidates []candidate
|
|
var orientation uint16
|
|
|
|
queue := []uint32{order.Uint32(data[4:8])}
|
|
seen := map[uint32]struct{}{}
|
|
// push queues an IFD offset unless the queue is already full; a crafted
|
|
// file with millions of SubIFD pointers must not grow it without bound
|
|
push := func(offset uint32) {
|
|
if len(queue) < maxIFDs {
|
|
queue = append(queue, offset)
|
|
}
|
|
}
|
|
first := true
|
|
for len(queue) > 0 && len(seen) < maxIFDs {
|
|
ifdOffset := queue[0]
|
|
queue = queue[1:]
|
|
if _, ok := seen[ifdOffset]; ok {
|
|
continue
|
|
}
|
|
seen[ifdOffset] = struct{}{}
|
|
if ifdOffset == 0 || int(ifdOffset)+2 > len(data) {
|
|
continue
|
|
}
|
|
entryCount := int(order.Uint16(data[ifdOffset : ifdOffset+2]))
|
|
entriesEnd := int(ifdOffset) + 2 + entryCount*12
|
|
if entriesEnd+4 > len(data) {
|
|
continue
|
|
}
|
|
|
|
var jpegOffset, jpegLength, stripOffset, stripLength uint32
|
|
for i := 0; i < entryCount; i++ {
|
|
entry := data[int(ifdOffset)+2+i*12:]
|
|
tag := order.Uint16(entry[0:2])
|
|
typ := order.Uint16(entry[2:4])
|
|
count := order.Uint32(entry[4:8])
|
|
switch tag {
|
|
case tiffTagOrientation:
|
|
// only IFD0's orientation applies
|
|
if first && typ == tiffTypeShort && count == 1 {
|
|
orientation = order.Uint16(entry[8:10])
|
|
}
|
|
case tiffTagJPEGOffset:
|
|
if typ == tiffTypeLong && count == 1 {
|
|
jpegOffset = order.Uint32(entry[8:12])
|
|
}
|
|
case tiffTagJPEGLength:
|
|
if typ == tiffTypeLong && count == 1 {
|
|
jpegLength = order.Uint32(entry[8:12])
|
|
}
|
|
case tiffTagStripOffsets:
|
|
// only single-strip images can be a contiguous JPEG stream
|
|
if typ == tiffTypeLong && count == 1 {
|
|
stripOffset = order.Uint32(entry[8:12])
|
|
}
|
|
case tiffTagStripByteCounts:
|
|
if typ == tiffTypeLong && count == 1 {
|
|
stripLength = order.Uint32(entry[8:12])
|
|
}
|
|
case tiffTagSubIFDs:
|
|
if typ != tiffTypeLong {
|
|
continue
|
|
}
|
|
if count == 1 {
|
|
push(order.Uint32(entry[8:12]))
|
|
continue
|
|
}
|
|
arrayOffset := order.Uint32(entry[8:12])
|
|
for j := uint32(0); j < count && j < maxIFDs; j++ {
|
|
pos := int(arrayOffset) + int(j)*4
|
|
if pos+4 > len(data) || len(queue) >= maxIFDs {
|
|
break
|
|
}
|
|
push(order.Uint32(data[pos : pos+4]))
|
|
}
|
|
}
|
|
}
|
|
if jpegOffset > 0 && jpegLength > 0 {
|
|
candidates = append(candidates, candidate{jpegOffset, jpegLength})
|
|
}
|
|
if stripOffset > 0 && stripLength > 0 {
|
|
candidates = append(candidates, candidate{stripOffset, stripLength})
|
|
}
|
|
push(order.Uint32(data[entriesEnd : entriesEnd+4]))
|
|
first = false
|
|
}
|
|
|
|
var best []byte
|
|
for _, c := range candidates {
|
|
end := int64(c.offset) + int64(c.length)
|
|
if end > int64(len(data)) || int(c.length) <= len(best) {
|
|
continue
|
|
}
|
|
jpg := data[c.offset:end]
|
|
if !isRenderableJPEG(jpg) {
|
|
continue
|
|
}
|
|
best = jpg
|
|
}
|
|
if best == nil {
|
|
return nil, 0, thumbnailerErrors.ErrNoImageFromRawFile
|
|
}
|
|
return best, orientation, nil
|
|
}
|
|
|
|
// isRenderableJPEG accepts only DCT processes common decoders render; DNG
|
|
// raw payloads are lossless JPEG (SOF3) behind a regular SOI marker.
|
|
func isRenderableJPEG(buf []byte) bool {
|
|
if len(buf) < 4 || buf[0] != 0xff || buf[1] != 0xd8 {
|
|
return false
|
|
}
|
|
i := 2
|
|
for segments := 0; i+4 <= len(buf) && segments < maxJPEGSegments; segments++ {
|
|
if buf[i] != 0xff {
|
|
return false
|
|
}
|
|
marker := buf[i+1]
|
|
switch {
|
|
case marker == 0xff: // fill byte
|
|
i++
|
|
continue
|
|
case marker >= 0xd0 && marker <= 0xd7: // RST, no length field
|
|
i += 2
|
|
continue
|
|
}
|
|
switch marker {
|
|
case 0xc0, 0xc1, 0xc2: // baseline, extended sequential, progressive
|
|
return true
|
|
case 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf:
|
|
// lossless, differential and arithmetic processes
|
|
return false
|
|
case 0xd9, 0xda: // EOI or scan start without a SOF
|
|
return false
|
|
}
|
|
segLen := int(buf[i+2])<<8 | int(buf[i+3])
|
|
if segLen < 2 {
|
|
return false
|
|
}
|
|
i += 2 + segLen
|
|
}
|
|
return false
|
|
}
|
|
|
|
// spliceJPEGOrientation inserts a minimal EXIF APP1 segment carrying only the
|
|
// orientation tag right after the SOI marker
|
|
func spliceJPEGOrientation(jpg []byte, orientation uint16) []byte {
|
|
if len(jpg) < 2 || jpg[0] != 0xff || jpg[1] != 0xd8 {
|
|
return jpg
|
|
}
|
|
tiff := make([]byte, 0, 26)
|
|
le := binary.LittleEndian
|
|
tiff = append(tiff, 'I', 'I')
|
|
tiff = le.AppendUint16(tiff, 42)
|
|
tiff = le.AppendUint32(tiff, 8) // IFD0 offset
|
|
tiff = le.AppendUint16(tiff, 1) // one entry
|
|
tiff = le.AppendUint16(tiff, tiffTagOrientation)
|
|
tiff = le.AppendUint16(tiff, tiffTypeShort)
|
|
tiff = le.AppendUint32(tiff, 1)
|
|
tiff = le.AppendUint16(tiff, orientation)
|
|
tiff = le.AppendUint16(tiff, 0) // value padding
|
|
tiff = le.AppendUint32(tiff, 0) // no next IFD
|
|
|
|
payload := append([]byte("Exif\x00\x00"), tiff...)
|
|
out := make([]byte, 0, len(jpg)+4+len(payload))
|
|
out = append(out, jpg[:2]...)
|
|
out = append(out, 0xff, 0xe1, byte((len(payload)+2)>>8), byte(len(payload)+2))
|
|
out = append(out, payload...)
|
|
return append(out, jpg[2:]...)
|
|
}
|