Skip to content

Commit 52627fc

Browse files
authored
Add constructor parameter EncoderDecoder.Config.maxEncodeEmit (#223)
1 parent ad5027c commit 52627fc

File tree

10 files changed

+191
-26
lines changed

10 files changed

+191
-26
lines changed

library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Base16.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ public class Base16: EncoderDecoder<Base16.Config> {
203203
lineBreakResetOnFlush,
204204
paddingChar = null,
205205
maxDecodeEmit = 1,
206+
maxEncodeEmit = 2,
206207
backFillBuffers,
207208
) {
208209

library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Base32.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ public sealed class Base32<C: EncoderDecoder.Config>(config: C): EncoderDecoder<
236236
lineBreakResetOnFlush = false,
237237
paddingChar = null,
238238
maxDecodeEmit = 5,
239+
maxEncodeEmit = calculateMaxEncodeEmit(emitSize = 8, insertionInterval = hyphenInterval.toInt()),
239240
backFillBuffers,
240241
) {
241242

@@ -642,6 +643,7 @@ public sealed class Base32<C: EncoderDecoder.Config>(config: C): EncoderDecoder<
642643
lineBreakResetOnFlush,
643644
paddingChar = '=',
644645
maxDecodeEmit = 5,
646+
maxEncodeEmit = 8,
645647
backFillBuffers,
646648
) {
647649

@@ -983,6 +985,7 @@ public sealed class Base32<C: EncoderDecoder.Config>(config: C): EncoderDecoder<
983985
lineBreakResetOnFlush,
984986
paddingChar = '=',
985987
maxDecodeEmit = 5,
988+
maxEncodeEmit = 8,
986989
backFillBuffers,
987990
) {
988991

library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/internal/-Config.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ internal inline fun ((Boolean, Boolean, Byte, Char?, Boolean, Boolean) -> Base32
2525
b: Base32.Crockford.Builder,
2626
noinline crockford: (Base32.Crockford.Config, Any?) -> Base32.Crockford,
2727
): Base32.Crockford {
28+
val hyphenInterval = if (b._hyphenInterval <= 0) 0 else b._hyphenInterval
2829
if (
2930
b._isLenient == Base32.Crockford.DELEGATE.config.isLenient
3031
&& b._encodeLowercase == Base32.Crockford.DELEGATE.config.encodeLowercase
31-
&& b._hyphenInterval == Base32.Crockford.DELEGATE.config.hyphenInterval
32+
&& hyphenInterval == Base32.Crockford.DELEGATE.config.hyphenInterval
3233
&& b._checkSymbol == Base32.Crockford.DELEGATE.config.checkSymbol
3334
&& b._finalizeWhenFlushed == Base32.Crockford.DELEGATE.config.finalizeWhenFlushed
3435
&& b._backFillBuffers == Base32.Crockford.DELEGATE.config.backFillBuffers
@@ -38,7 +39,7 @@ internal inline fun ((Boolean, Boolean, Byte, Char?, Boolean, Boolean) -> Base32
3839
val config = this(
3940
b._isLenient,
4041
b._encodeLowercase,
41-
b._hyphenInterval,
42+
hyphenInterval,
4243
b._checkSymbol,
4344
b._finalizeWhenFlushed,
4445
b._backFillBuffers,

library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Base64.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ public class Base64: EncoderDecoder<Base64.Config> {
254254
lineBreakResetOnFlush,
255255
paddingChar = '=',
256256
maxDecodeEmit = 3,
257+
maxEncodeEmit = 4,
257258
backFillBuffers,
258259
) {
259260

library/core/api/core.api

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,11 @@ public abstract class io/matthewnelson/encoding/core/EncoderDecoder$Config {
124124
public final field lineBreakInterval B
125125
public final field lineBreakResetOnFlush Z
126126
public final field maxDecodeEmit I
127+
public final field maxEncodeEmit I
127128
public final field paddingChar Ljava/lang/Character;
128129
public fun <init> (Ljava/lang/Boolean;BLjava/lang/Character;)V
129-
protected fun <init> (Ljava/lang/Boolean;BZLjava/lang/Character;IZ)V
130+
protected fun <init> (Ljava/lang/Boolean;BZLjava/lang/Character;IIZ)V
131+
public static final fun calculateMaxEncodeEmit (II)I
130132
public final fun decodeOutMaxSize (J)J
131133
public final fun decodeOutMaxSizeOrFail (Lio/matthewnelson/encoding/core/util/DecoderInput;)I
132134
protected abstract fun decodeOutMaxSizeOrFailProtected (ILio/matthewnelson/encoding/core/util/DecoderInput;)I
@@ -144,6 +146,7 @@ public abstract class io/matthewnelson/encoding/core/EncoderDecoder$Config {
144146
}
145147

146148
public final class io/matthewnelson/encoding/core/EncoderDecoder$Config$Companion {
149+
public final fun calculateMaxEncodeEmit (II)I
147150
public final fun outSizeExceedsMaxEncodingSizeException (Ljava/lang/Number;Ljava/lang/Number;)Lio/matthewnelson/encoding/core/EncodingSizeException;
148151
}
149152

library/core/api/core.klib.api

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ abstract class <#A: io.matthewnelson.encoding.core/EncoderDecoder.Config> io.mat
3636
final fun toString(): kotlin/String // io.matthewnelson.encoding.core/EncoderDecoder.toString|toString(){}[0]
3737

3838
abstract class Config { // io.matthewnelson.encoding.core/EncoderDecoder.Config|null[0]
39-
constructor <init>(kotlin/Boolean?, kotlin/Byte, kotlin/Boolean, kotlin/Char?, kotlin/Int, kotlin/Boolean) // io.matthewnelson.encoding.core/EncoderDecoder.Config.<init>|<init>(kotlin.Boolean?;kotlin.Byte;kotlin.Boolean;kotlin.Char?;kotlin.Int;kotlin.Boolean){}[0]
39+
constructor <init>(kotlin/Boolean?, kotlin/Byte, kotlin/Boolean, kotlin/Char?, kotlin/Int, kotlin/Int, kotlin/Boolean) // io.matthewnelson.encoding.core/EncoderDecoder.Config.<init>|<init>(kotlin.Boolean?;kotlin.Byte;kotlin.Boolean;kotlin.Char?;kotlin.Int;kotlin.Int;kotlin.Boolean){}[0]
4040
constructor <init>(kotlin/Boolean?, kotlin/Byte, kotlin/Char?) // io.matthewnelson.encoding.core/EncoderDecoder.Config.<init>|<init>(kotlin.Boolean?;kotlin.Byte;kotlin.Char?){}[0]
4141

4242
final val backFillBuffers // io.matthewnelson.encoding.core/EncoderDecoder.Config.backFillBuffers|{}backFillBuffers[0]
@@ -49,6 +49,8 @@ abstract class <#A: io.matthewnelson.encoding.core/EncoderDecoder.Config> io.mat
4949
final fun <get-lineBreakResetOnFlush>(): kotlin/Boolean // io.matthewnelson.encoding.core/EncoderDecoder.Config.lineBreakResetOnFlush.<get-lineBreakResetOnFlush>|<get-lineBreakResetOnFlush>(){}[0]
5050
final val maxDecodeEmit // io.matthewnelson.encoding.core/EncoderDecoder.Config.maxDecodeEmit|{}maxDecodeEmit[0]
5151
final fun <get-maxDecodeEmit>(): kotlin/Int // io.matthewnelson.encoding.core/EncoderDecoder.Config.maxDecodeEmit.<get-maxDecodeEmit>|<get-maxDecodeEmit>(){}[0]
52+
final val maxEncodeEmit // io.matthewnelson.encoding.core/EncoderDecoder.Config.maxEncodeEmit|{}maxEncodeEmit[0]
53+
final fun <get-maxEncodeEmit>(): kotlin/Int // io.matthewnelson.encoding.core/EncoderDecoder.Config.maxEncodeEmit.<get-maxEncodeEmit>|<get-maxEncodeEmit>(){}[0]
5254
final val paddingChar // io.matthewnelson.encoding.core/EncoderDecoder.Config.paddingChar|{}paddingChar[0]
5355
final fun <get-paddingChar>(): kotlin/Char? // io.matthewnelson.encoding.core/EncoderDecoder.Config.paddingChar.<get-paddingChar>|<get-paddingChar>(){}[0]
5456

@@ -80,6 +82,7 @@ abstract class <#A: io.matthewnelson.encoding.core/EncoderDecoder.Config> io.mat
8082
}
8183

8284
final object Companion { // io.matthewnelson.encoding.core/EncoderDecoder.Config.Companion|null[0]
85+
final fun calculateMaxEncodeEmit(kotlin/Int, kotlin/Int): kotlin/Int // io.matthewnelson.encoding.core/EncoderDecoder.Config.Companion.calculateMaxEncodeEmit|calculateMaxEncodeEmit(kotlin.Int;kotlin.Int){}[0]
8386
final fun outSizeExceedsMaxEncodingSizeException(kotlin/Number, kotlin/Number): io.matthewnelson.encoding.core/EncodingSizeException // io.matthewnelson.encoding.core/EncoderDecoder.Config.Companion.outSizeExceedsMaxEncodingSizeException|outSizeExceedsMaxEncodingSizeException(kotlin.Number;kotlin.Number){}[0]
8487
}
8588
}

library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt

Lines changed: 128 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import io.matthewnelson.encoding.core.internal.closedException
2222
import io.matthewnelson.encoding.core.internal.isSpaceOrNewLine
2323
import io.matthewnelson.encoding.core.util.DecoderInput
2424
import io.matthewnelson.encoding.core.util.LineBreakOutFeed
25+
import kotlin.contracts.ExperimentalContracts
26+
import kotlin.contracts.InvocationKind
27+
import kotlin.contracts.contract
2528
import kotlin.jvm.JvmField
2629
import 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
699791
private 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

Comments
 (0)