11package org.monogram.feature.dialog
22
3- import java.io.ByteArrayInputStream
43import java.io.ByteArrayOutputStream
54import java.io.DataInputStream
65import java.io.DataOutputStream
76import java.io.File
87import java.util.zip.Deflater
98import java.util.zip.DeflaterOutputStream
10- import java.util.zip.InflaterInputStream
9+ import java.util.zip.Inflater
1110import kotlin.math.max
1211import kotlin.math.roundToInt
1312
@@ -16,13 +15,19 @@ internal data class PanelFrameClip(
1615 val height : Int ,
1716 val delayMs : Int ,
1817 val frames : List <ByteArray >,
18+ val complete : Boolean = false ,
19+ val sourceDurationMs : Long = 0L ,
1920)
2021
2122internal object PanelFrameCache {
23+ internal const val PACKED_BYTE_CAP = 32 * 1024 * 1024
24+ internal const val INFLATED_BYTE_CAP = 512 * 512 * 4 * 90
25+
2226 private val MAGIC =
23- byteArrayOf(' P' .code.toByte(), ' F' .code.toByte(), ' 0' .code.toByte(), ' 1 ' .code.toByte())
27+ byteArrayOf(' P' .code.toByte(), ' F' .code.toByte(), ' 0' .code.toByte(), ' 2 ' .code.toByte())
2428 private const val MAX_FRAMES = 90
2529 private const val MAX_SIDE = 512
30+ private const val HEADER_BYTES = 33L
2631 private const val PANEL_FRAME_BYTE_CAP = 32L * 1024 * 1024
2732
2833 fun file (dir : File , documentId : Long , bucket : PanelBucket , tag : PanelTag ): File =
@@ -39,6 +44,8 @@ internal object PanelFrameCache {
3944 data.writeInt(clip.height)
4045 data.writeInt(clip.delayMs.coerceAtLeast(1 ))
4146 data.writeInt(clip.frames.size)
47+ data.writeBoolean(clip.complete)
48+ data.writeLong(clip.sourceDurationMs.coerceAtLeast(0L ))
4249 val packed = ByteArrayOutputStream ()
4350 DeflaterOutputStream (packed, Deflater (Deflater .BEST_SPEED )).use { deflater ->
4451 clip.frames.forEach { frame -> deflater.write(frame) }
@@ -62,8 +69,15 @@ internal object PanelFrameCache {
6269 )
6370 }
6471
65- fun read (source : File ): PanelFrameClip ? {
66- if (! source.isFile || source.length() < 24L ) return null
72+ fun read (source : File ): PanelFrameClip ? = readBounded(source, requestedSide = null )
73+
74+ fun read (source : File , requestedSide : Int ): PanelFrameClip ? {
75+ if (requestedSide <= 0 ) return null
76+ return readBounded(source, requestedSide)
77+ }
78+
79+ private fun readBounded (source : File , requestedSide : Int? ): PanelFrameClip ? {
80+ if (! source.isFile || source.length() < HEADER_BYTES ) return null
6781 return runCatching {
6882 source.inputStream().buffered().use { stream ->
6983 val data = DataInputStream (stream)
@@ -74,34 +88,57 @@ internal object PanelFrameCache {
7488 val height = data.readInt()
7589 val delay = data.readInt()
7690 val count = data.readInt()
91+ val complete = data.readBoolean()
92+ val sourceDurationMs = data.readLong()
7793 val packedSize = data.readInt()
94+ if (sourceDurationMs < 0L ) return null
7895 if (! validShape(width, height, count, packedSize)) return null
96+ if (requestedSide != null && max(width, height) < requestedSide) return null
97+ val inflated = width.toLong() * height.toLong() * 4L * count.toLong()
98+ if (inflated <= 0L || inflated > INFLATED_BYTE_CAP ) return null
99+ if (packedSize.toLong() > source.length() - HEADER_BYTES ) return null
79100 val packed = ByteArray (packedSize)
80101 data.readFully(packed)
81- val frameBytes = width * height * 4
82- val raw = InflaterInputStream (ByteArrayInputStream (packed)).use { it.readBytes() }
83- if (raw.size != frameBytes * count) return null
102+ val raw = ByteArray (inflated.toInt())
103+ val inflater = Inflater ()
104+ try {
105+ inflater.setInput(packed)
106+ var offset = 0
107+ while (offset < raw.size) {
108+ val read = inflater.inflate(raw, offset, raw.size - offset)
109+ if (read <= 0 ) return null
110+ offset + = read
111+ }
112+ if (! inflater.finished()) {
113+ val extra = inflater.inflate(ByteArray (1 ))
114+ if (extra != 0 || ! inflater.finished()) return null
115+ }
116+ } finally {
117+ inflater.end()
118+ }
119+ val frameBytes = (inflated / count).toInt()
84120 val frames = ArrayList <ByteArray >(count)
85121 for (index in 0 until count) {
86122 val start = index * frameBytes
87123 frames + = raw.copyOfRange(start, start + frameBytes)
88124 }
89- PanelFrameClip (width, height, delay, frames)
125+ PanelFrameClip (width, height, delay, frames, complete, sourceDurationMs )
90126 }
91127 }.getOrNull()
92128 }
93129
94130 private fun storeable (clip : PanelFrameClip ): Boolean {
131+ if (clip.sourceDurationMs < 0L ) return false
95132 if (! validShape(clip.width, clip.height, clip.frames.size, packedSize = 1 )) return false
96133 val frameBytes = clip.width * clip.height * 4
97134 return clip.frames.all { it.size == frameBytes }
98135 }
99136
100137 private fun validShape (width : Int , height : Int , count : Int , packedSize : Int ): Boolean {
101138 if (width !in 1 .. MAX_SIDE || height !in 1 .. MAX_SIDE ) return false
102- if (count !in 1 .. MAX_FRAMES || packedSize <= 0 ) return false
103- val area = width.toLong() * height.toLong()
104- return area <= MAX_SIDE .toLong() * MAX_SIDE .toLong()
139+ if (count !in 1 .. MAX_FRAMES || packedSize <= 0 || packedSize > PACKED_BYTE_CAP ) return false
140+ val inflated = width.toLong() * height.toLong() * 4L * count .toLong()
141+ return inflated in 1 .. INFLATED_BYTE_CAP .toLong()
105142 }
106143}
107144
0 commit comments