@@ -22,6 +22,9 @@ import io.matthewnelson.encoding.core.internal.closedException
2222import io.matthewnelson.encoding.core.internal.isSpaceOrNewLine
2323import io.matthewnelson.encoding.core.util.DecoderInput
2424import io.matthewnelson.encoding.core.util.LineBreakOutFeed
25+ import kotlin.contracts.ExperimentalContracts
26+ import kotlin.contracts.InvocationKind
27+ import kotlin.contracts.contract
2528import kotlin.jvm.JvmField
2629import kotlin.jvm.JvmStatic
2730
@@ -109,12 +112,39 @@ public abstract class EncoderDecoder<C: EncoderDecoder.Config>(config: C): Encod
109112 *
110113 * Value will be between `1` and `255` (inclusive), or `-1` which indicates that the
111114 * [EncoderDecoder.Config] implementation has not updated to the new constructor introduced
112- * in version `2.6.0` and as such is unable to be used with `:core` module APIs dependent
115+ * in version `2.6.0`, and as such is unable to be used with `:core` module APIs dependent
113116 * on this value (such as [Decoder.decodeBuffered] or [Decoder.decodeBufferedAsync]).
114117 * */
115118 @JvmField
116119 public val maxDecodeEmit : Int ,
117120
121+ /* *
122+ * The maximum number of characters that the implementation's [Encoder.Feed] can
123+ * potentially emit on a single invocation of [Encoder.Feed.consume], [Encoder.Feed.flush],
124+ * or [Encoder.Feed.doFinal].
125+ *
126+ * For example, `Base16` encoding will emit `2` characters for every `1` byte of input,
127+ * so its maximum emission is `2`. `Base32` encoding will emit `8` characters for every
128+ * `5` bytes of input, so its maximum emission is `8`. `UTF8` "encoding" (i.e. UTF-8 byte
129+ * to text transformations) can emit `4` characters for every `4` bytes of input (depending
130+ * on the implementation), so its maximum emission would be `4`.
131+ *
132+ * **NOTE:** This value does **not** take into consideration the [lineBreakInterval] setting,
133+ * or any other [LineBreakOutFeed]-like implementation that may inflate the maximum character
134+ * emission size. Implementations must **only** consider their own [LineBreakOutFeed]-like
135+ * implementation details (such as the hyphen interval for `Base32.Crockford`) when calculating
136+ * their maximum character emission size.
137+ *
138+ * Value will be between `1` and `255` (inclusive), or `-1` which indicates that the
139+ * [EncoderDecoder.Config] implementation has not updated to the new constructor introduced
140+ * in version `2.6.0`, and as such is unable to be used with `:core` module APIs dependent
141+ * on this value.
142+ *
143+ * @see [Companion.calculateMaxEncodeEmit]
144+ * */
145+ @JvmField
146+ public val maxEncodeEmit : Int ,
147+
118148 /* *
119149 * When the functions [Encoder.encodeToString], [Encoder.encodeToCharArray],
120150 * [Decoder.decodeToByteArray], [Decoder.decodeBuffered], and [Decoder.decodeBufferedAsync]
@@ -139,26 +169,29 @@ public abstract class EncoderDecoder<C: EncoderDecoder.Config>(config: C): Encod
139169 /* *
140170 * Instantiates a new [Config] instance.
141171 *
142- * @throws [IllegalArgumentException] If [maxDecodeEmit] is less than `1` or greater than `255`.
172+ * @throws [IllegalArgumentException] If [maxDecodeEmit] is less than `1` or greater than
173+ * `255`. If [maxEncodeEmit] is less than `1` or greater than `255`.
143174 * */
144175 protected constructor (
145176 isLenient: Boolean? ,
146177 lineBreakInterval: Byte ,
147178 lineBreakResetOnFlush: Boolean ,
148179 paddingChar: Char? ,
149180 maxDecodeEmit: Int ,
181+ maxEncodeEmit: Int ,
150182 backFillBuffers: Boolean ,
151183 ): this (
152184 isLenient = isLenient,
153185 lineBreakInterval = lineBreakIntervalOrZero(isLenient, lineBreakInterval),
154186 lineBreakResetOnFlush = lineBreakResetOnFlush,
155187 paddingChar = paddingChar,
156188 maxDecodeEmit = maxDecodeEmit,
189+ maxEncodeEmit = maxEncodeEmit,
157190 backFillBuffers = backFillBuffers,
158191 unused = null ,
159192 ) {
160- require (maxDecodeEmit > 0 ) { " maxDecodeEmit must be greater than 0 " }
161- require(maxDecodeEmit < 256 ) { " maxDecodeEmit must be less than 256 " }
193+ checkMaxEmitSize (maxDecodeEmit) { " maxDecodeEmit" }
194+ checkMaxEmitSize(maxEncodeEmit ) { " maxEncodeEmit " }
162195 }
163196
164197 /* *
@@ -339,6 +372,76 @@ public abstract class EncoderDecoder<C: EncoderDecoder.Config>(config: C): Encod
339372 return outSize
340373 }
341374
375+ public companion object {
376+
377+ /* *
378+ * Calculates and returns the maximum character emission size, given some sort of
379+ * [emitSize] and desire to insert characters every [insertionInterval]. The way
380+ * things are calculated are based on how [lineBreakInterval] operates, whereby
381+ * if [insertionInterval] encoded characters have been output, the next encoded
382+ * character output will be preceded with some arbitrary character (such as a
383+ * hyphen for `Base32.Crockford`, which uses this function to calculate its final
384+ * [maxEncodeEmit] value passed to the [Config] constructor).
385+ *
386+ * **NOTE:** Implementors of [Config] utilizing this to calculate their [maxEncodeEmit]
387+ * values must consider that it may return a value greater than `255`, depending on the
388+ * input arguments, resulting in an [IllegalArgumentException] when passed into the
389+ * [Config] constructor. For example, an [insertionInterval] of `1` will inflate the
390+ * provided [emitSize] by 2x.
391+ *
392+ * @param [emitSize] The number of characters that are expected to be emitted.
393+ * @param [insertionInterval] The interval at which `1` character is to be inserted.
394+ *
395+ * @return The calculated emission size for a provided character [insertionInterval], or
396+ * [emitSize] itself if [insertionInterval] is less than `1`.
397+ *
398+ * @see [lineBreakInterval]
399+ * @see [maxEncodeEmit]
400+ *
401+ * @throws [IllegalArgumentException] If [emitSize] is less than `1` or greater than `255`.
402+ * */
403+ @JvmStatic
404+ public fun calculateMaxEncodeEmit (emitSize : Int , insertionInterval : Int ): Int {
405+ checkMaxEmitSize(emitSize) { " emitSize" }
406+ if (insertionInterval <= 0 ) return emitSize
407+ if (insertionInterval >= emitSize) return emitSize + 1
408+
409+ // Starting count at insertionInterval instead of 0 simulates the case of
410+ // the very next output of emitSize encoded characters is to be preceded
411+ // by the insertion character, such as a new line `\n`, whereby the max
412+ // can be calculated.
413+ //
414+ // The limits for when this run an emitSize of 255 and an insertionInterval
415+ // of emitSize - 1. Given how small actual emitSizes are expected to be,
416+ // such as 8 for Base32, calculating things this way is OK with me...
417+ var count = insertionInterval
418+ var output = 0
419+ var i = 0
420+ while (i++ < emitSize) {
421+ if (count == insertionInterval) {
422+ output++
423+ count = 0
424+ }
425+ output++
426+ count++
427+ }
428+ return output
429+ }
430+
431+ /* *
432+ * Helper for generating an [EncodingSizeException] when the
433+ * pre-calculated encoded/decoded output size exceeds the maximum for
434+ * the given encoding/decoding specification.
435+ * */
436+ @JvmStatic
437+ public fun outSizeExceedsMaxEncodingSizeException (
438+ inputSize : Number ,
439+ maxSize : Number ,
440+ ): EncodingSizeException = EncodingSizeException (
441+ " Size[$inputSize ] of input would exceed the maximum output Size[$maxSize ] for this operation."
442+ )
443+ }
444+
342445 /* *
343446 * Calculate and return an exact (preferably), or maximum, size that an encoding would be
344447 * for the [unEncodedSize] data.
@@ -430,6 +533,7 @@ public abstract class EncoderDecoder<C: EncoderDecoder.Config>(config: C): Encod
430533 if (other.lineBreakResetOnFlush != this .lineBreakResetOnFlush) return false
431534 if (other.paddingChar != this .paddingChar) return false
432535 if (other.maxDecodeEmit != this .maxDecodeEmit) return false
536+ if (other.maxEncodeEmit != this .maxEncodeEmit) return false
433537 if (other.backFillBuffers != this .backFillBuffers) return false
434538 if (other::class != this ::class ) return false
435539 return other._toStringAddSettings == this ._toStringAddSettings
@@ -443,6 +547,7 @@ public abstract class EncoderDecoder<C: EncoderDecoder.Config>(config: C): Encod
443547 result = result * 31 + lineBreakResetOnFlush.hashCode()
444548 result = result * 31 + paddingChar.hashCode()
445549 result = result * 31 + maxDecodeEmit.hashCode()
550+ result = result * 31 + maxEncodeEmit.hashCode()
446551 result = result * 31 + backFillBuffers.hashCode()
447552 result = result * 31 + this ::class .hashCode()
448553 result = result * 31 + _toStringAddSettings .hashCode()
@@ -462,6 +567,8 @@ public abstract class EncoderDecoder<C: EncoderDecoder.Config>(config: C): Encod
462567 appendLine(paddingChar)
463568 append(" maxDecodeEmit: " )
464569 appendLine(maxDecodeEmit)
570+ append(" maxEncodeEmit: " )
571+ appendLine(maxEncodeEmit)
465572 append(" backFillBuffers: " )
466573 append(backFillBuffers) // last one uses append, not appendLine
467574
@@ -475,22 +582,6 @@ public abstract class EncoderDecoder<C: EncoderDecoder.Config>(config: C): Encod
475582 append(' ]' )
476583 }.toString()
477584
478- public companion object {
479-
480- /* *
481- * Helper for generating an [EncodingSizeException] when the
482- * pre-calculated encoded/decoded output size exceeds the maximum for
483- * the given encoding/decoding specification.
484- * */
485- @JvmStatic
486- public fun outSizeExceedsMaxEncodingSizeException (
487- inputSize : Number ,
488- maxSize : Number ,
489- ): EncodingSizeException = EncodingSizeException (
490- " Size[$inputSize ] of input would exceed the maximum output Size[$maxSize ] for this operation."
491- )
492- }
493-
494585 /* *
495586 * DEPRECATED since `2.6.0`
496587 * @see [encodeOutMaxSize]
@@ -520,9 +611,9 @@ public abstract class EncoderDecoder<C: EncoderDecoder.Config>(config: C): Encod
520611 * @suppress
521612 * */
522613 @Deprecated(
523- message = " Parameters, lineBreakResetOnFlush, maxDecodeEmit, and backFillBuffers were added. Use the new constructor." ,
614+ message = " Parameters, lineBreakResetOnFlush, maxDecodeEmit, maxEncodeEmit, and backFillBuffers were added. Use the new constructor." ,
524615 replaceWith = ReplaceWith (
525- expression = " EncoderDecoder.Config(isLenient, lineBreakInterval, lineBreakResetOnFlush = false, paddingChar, maxDecodeEmit = 0 /* TODO */, backFillBuffers = true)" ),
616+ expression = " EncoderDecoder.Config(isLenient, lineBreakInterval, lineBreakResetOnFlush = false, paddingChar, maxDecodeEmit = 0 /* TODO */, maxEncodeEmit = 0 /* TODO */, backFillBuffers = true)" ),
526617 level = DeprecationLevel .WARNING ,
527618 )
528619 public constructor (
@@ -535,6 +626,7 @@ public abstract class EncoderDecoder<C: EncoderDecoder.Config>(config: C): Encod
535626 lineBreakResetOnFlush = false ,
536627 paddingChar = paddingChar,
537628 maxDecodeEmit = - 1 , // NOTE: NEVER change.
629+ maxEncodeEmit = - 1 , // NOTE: NEVER change.
538630 backFillBuffers = true ,
539631 unused = null ,
540632 )
@@ -699,3 +791,17 @@ public abstract class EncoderDecoder<C: EncoderDecoder.Config>(config: C): Encod
699791private inline fun lineBreakIntervalOrZero (isLenient : Boolean? , interval : Byte ): Byte {
700792 return if (isLenient != false && interval > 0 ) interval else 0
701793}
794+
795+ @OptIn(ExperimentalContracts ::class )
796+ @Throws(IllegalArgumentException ::class )
797+ private inline fun checkMaxEmitSize (size : Int , parameterName : () -> String ) {
798+ contract { callsInPlace(parameterName, InvocationKind .AT_MOST_ONCE ) }
799+ if (size <= 0 ) {
800+ val n = parameterName()
801+ throw IllegalArgumentException (" $n must be greater than 0" )
802+ }
803+ if (size >= 256 ) {
804+ val n = parameterName()
805+ throw IllegalArgumentException (" $n must be less than 256" )
806+ }
807+ }
0 commit comments