Skip to content

Commit

Permalink
Add valid/in-enum to validate enum values
Browse files Browse the repository at this point in the history
  • Loading branch information
generalmimon committed Aug 19, 2024
1 parent 3c855b3 commit 793ba60
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ class GraphvizClassCompiler(classSpecs: ClassSpecs, topClass: ClassSpec) extends
s"and ${expression(max, fullPortName, STYLE_EDGE_VALID)}"
case ValidationAnyOf(values) =>
s"must be any of ${values.map(expression(_, fullPortName, STYLE_EDGE_VALID)).mkString(", ")}"
case ValidationInEnum() =>
"must be defined in the enum"
case ValidationExpr(expr) =>
provider._currentIteratorType = Some(dataType)
s"must satisfy ${expression(expr, fullPortName, STYLE_EDGE_VALID)}"
Expand Down
4 changes: 2 additions & 2 deletions shared/src/main/scala/io/kaitai/struct/RuntimeConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ package io.kaitai.struct
* @param stdStringFrontBack If true, allow use of `front()` and `back()` methods
* on `std::string`. If false, come up with simulation.
* @param useListInitializers If true, allows use of list initializers for
* `std::vector`. Otherwise, throw a fatal unimplemented
* error.
* `std::vector` and `std::set`. Otherwise, throw a fatal
* "not implemented" error.
* @param pointers Choose which style of pointers to use.
*/
case class CppRuntimeConfig(
Expand Down
9 changes: 9 additions & 0 deletions shared/src/main/scala/io/kaitai/struct/datatype/KSError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ object KSError {
case "ValidationLessThanError" => ValidationLessThanError
case "ValidationGreaterThanError" => ValidationGreaterThanError
case "ValidationNotAnyOfError" => ValidationNotAnyOfError
case "ValidationNotInEnumError" => ValidationNotInEnumError
case "ValidationExprError" => ValidationExprError
}
excClass(dataType)
Expand Down Expand Up @@ -66,6 +67,14 @@ case class ValidationNotAnyOfError(_dt: DataType) extends ValidationError(_dt) {
def name = "ValidationNotAnyOfError"
}

/**
* Error to be thrown when validation fails with actual not being in the enum.
* @param _dt data type used in validation process
*/
case class ValidationNotInEnumError(_dt: DataType) extends ValidationError(_dt) {
def name = "ValidationNotInEnumError"
}

/**
* Error to be thrown when validation fails with actual not matching the custom
* validation expression.
Expand Down
29 changes: 25 additions & 4 deletions shared/src/main/scala/io/kaitai/struct/format/ValidationSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ case class ValidationMin(min: Ast.expr) extends ValidationSpec
case class ValidationMax(max: Ast.expr) extends ValidationSpec
case class ValidationRange(min: Ast.expr, max: Ast.expr) extends ValidationSpec
case class ValidationAnyOf(values: List[Ast.expr]) extends ValidationSpec
case class ValidationInEnum() extends ValidationSpec
case class ValidationExpr(checkExpr: Ast.expr) extends ValidationSpec

object ValidationEq {
Expand Down Expand Up @@ -59,13 +60,30 @@ object ValidationAnyOf {
}
}

object ValidationInEnum {
val LEGAL_KEYS = Set("in-enum")

def fromMap(src: Map[String, Any], path: List[String]): Option[ValidationInEnum] =
ParseUtils.getOptValueBool(src, "in-enum", path).map { case boolVal =>
ParseUtils.ensureLegalKeys(src, LEGAL_KEYS, path)
if (!boolVal) {
throw KSYParseError.withText(
"only `true` is supported as value, got `false`" +
" (if you don't want any validation, omit the `valid` key)",
path ++ List("in-enum")
)
}
ValidationInEnum()
}
}

object ValidationExpr {
val LEGAL_KEYS = Set("expr")

def fromMap(src: Map[String, Any], path: List[String]): Option[ValidationExpr] =
ParseUtils.getOptValueExpression(src, "expr", path).map { case eqExpr =>
ParseUtils.getOptValueExpression(src, "expr", path).map { case expr =>
ParseUtils.ensureLegalKeys(src, LEGAL_KEYS, path)
ValidationExpr(eqExpr)
ValidationExpr(expr)
}
}

Expand All @@ -74,6 +92,7 @@ object ValidationSpec {
ValidationEq.LEGAL_KEYS ++
ValidationRange.LEGAL_KEYS ++
ValidationAnyOf.LEGAL_KEYS ++
ValidationInEnum.LEGAL_KEYS ++
ValidationExpr.LEGAL_KEYS

def fromYaml(src: Any, path: List[String]): ValidationSpec = {
Expand Down Expand Up @@ -106,10 +125,12 @@ object ValidationSpec {
val opt3 = ValidationAnyOf.fromMap(src, path)
if (opt3.nonEmpty)
return opt3.get
val opt4 = ValidationExpr.fromMap(src, path)
val opt4 = ValidationInEnum.fromMap(src, path)
if (opt4.nonEmpty)
return opt4.get

val opt5 = ValidationExpr.fromMap(src, path)
if (opt5.nonEmpty)
return opt5.get
// No validation templates matched, check for any bogus keys
ParseUtils.ensureLegalKeys(src, LEGAL_KEYS, path)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -597,9 +597,31 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)
checkExpr: Ast.expr,
err: KSError,
errArgs: List[Ast.expr]
): Unit =
attrValidate(s"!(${translator.translate(checkExpr)})", err, errArgs)

override def attrValidateInEnum(
attrId: Identifier,
et: EnumType,
valueExpr: Ast.expr,
err: ValidationNotInEnumError,
errArgs: List[Ast.expr]
): Unit = {
// TODO: the non-generic overload `Enum.IsDefined(Type, object)` used here
// is supposedly slow because it uses reflection (see
// https://stackoverflow.com/q/13615#comment48178324_4807469). [This SO
// answer](https://stackoverflow.com/a/55028274) suggests to use the generic
// overload `Enum.IsDefined<TEnum>(TEnum)` instead, claiming that it fixes
// the performance issues. But it's only available since .NET 5, so we would
// need a command-line switch to allow the user to choose whether they need
// compabitility with older versions or not.
importList.add("System")
attrValidate(s"!Enum.IsDefined(typeof(${kaitaiType2NativeType(et)}), ${translator.translate(valueExpr)})", err, errArgs)
}

private def attrValidate(failCondExpr: String, err: KSError, errArgs: List[Ast.expr]): Unit = {
val errArgsStr = errArgs.map(translator.translate).mkString(", ")
out.puts(s"if (!(${translator.translate(checkExpr)}))")
out.puts(s"if ($failCondExpr)")
out.puts("{")
out.inc
out.puts(s"throw new ${ksErrorName(err)}($errArgsStr);")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,21 @@ class CppCompiler(

outHdr.dec
outHdr.puts("};")
if (config.cppConfig.useListInitializers) {
// NOTE: declaration and initialization must be separate in this case,
// see https://stackoverflow.com/a/12856069
importListHdr.addSystem("set")
importListHdr.addSystem("type_traits") // because of `std::underlying_type`
// NOTE: `std::underlying_type` is only available since C++11. At the time
// of writing, it's not a problem because we're relying on list
// initializers anyway (which is a C++11 feature), but it will become a
// problem when we add C++98 support here.
outHdr.puts(s"static const std::set<std::underlying_type<$enumClass>::type> _values_$enumClass;")
val enumClassAbs = types2class(curClass ++ List(enumName))
val valuesSetAbsRef = s"${types2class(curClass)}::_values_$enumClass"
val setEntriesStr = enumColl.map { case (id, _) => s"$id" }.mkString(", ")
outSrc.puts(s"const std::set<std::underlying_type<$enumClassAbs>::type> $valuesSetAbsRef{$setEntriesStr};")
}
}

override def classToString(toStringExpr: Ast.expr): Unit = {
Expand Down Expand Up @@ -1007,6 +1022,7 @@ class CppCompiler(
case _: ValidationLessThanError => "validation_less_than_error"
case _: ValidationGreaterThanError => "validation_greater_than_error"
case _: ValidationNotAnyOfError => "validation_not_any_of_error"
case _: ValidationNotInEnumError => "validation_not_in_enum_error"
case _: ValidationExprError => "validation_expr_error"
}
s"kaitai::$cppErrName<$cppType>"
Expand All @@ -1018,10 +1034,32 @@ class CppCompiler(
checkExpr: Ast.expr,
err: KSError,
errArgs: List[Ast.expr]
): Unit =
attrValidate(s"!(${translator.translate(checkExpr)})", err, errArgs)

override def attrValidateInEnum(
attrId: Identifier,
et: EnumType,
valueExpr: Ast.expr,
err: ValidationNotInEnumError,
errArgs: List[Ast.expr]
): Unit = {
if (!config.cppConfig.useListInitializers) {
throw new NotImplementedError(
"`valid/in-enum` is not yet implemented for C++98 (switch to C++11 or avoid this feature)"
)
}
val enumSpec = et.enumSpec.get
val inClassRef = types2class(enumSpec.name.dropRight(1))
val enumNameStr = type2class(enumSpec.name.last)
val valuesSetRef = s"$inClassRef::_values_$enumNameStr"
attrValidate(s"$valuesSetRef.find(${translator.translate(valueExpr)}) == $valuesSetRef.end()", err, errArgs)
}

private def attrValidate(failCondExpr: String, err: KSError, errArgs: List[Ast.expr]): Unit = {
val errArgsStr = errArgs.map(translator.translate).mkString(", ")
importListSrc.addKaitai("kaitai/exceptions.h")
outSrc.puts(s"if (!(${translator.translate(checkExpr)})) {")
outSrc.puts(s"if ($failCondExpr) {")
outSrc.inc
outSrc.puts(s"throw ${ksErrorName(err)}($errArgsStr);")
outSrc.dec
Expand Down
25 changes: 23 additions & 2 deletions shared/src/main/scala/io/kaitai/struct/languages/GoCompiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,15 @@ class GoCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)

out.dec
out.puts(")")
// Inspired by https://gist.github.com/bgadrian/cb8b9344d9c66571ef331a14eb7a2e80
val mapEntriesStr = enumColl.map { case (id, _) => s"$id: {}" }.mkString(", ")
out.puts(s"var values_$fullEnumNameStr = map[$fullEnumNameStr]struct{}{$mapEntriesStr}")
out.puts(s"func (v $fullEnumNameStr) isDefined() bool {")
out.inc
out.puts(s"_, ok := values_$fullEnumNameStr[v]")
out.puts("return ok")
out.dec
out.puts("}")
}

override def classToString(toStringExpr: Ast.expr): Unit = {
Expand Down Expand Up @@ -569,9 +578,21 @@ class GoCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)
checkExpr: Ast.expr,
err: KSError,
errArgs: List[Ast.expr]
): Unit = {
): Unit =
attrValidate(s"!(${translator.translate(checkExpr)})", err, errArgs)

override def attrValidateInEnum(
attrId: Identifier,
et: EnumType,
valueExpr: Ast.expr,
err: ValidationNotInEnumError,
errArgs: List[Ast.expr]
): Unit =
attrValidate(s"!${translator.translate(valueExpr)}.isDefined()", err, errArgs)

private def attrValidate(failCondExpr: String, err: KSError, errArgs: List[Ast.expr]): Unit = {
val errArgsStr = errArgs.map(translator.translate).mkString(", ")
out.puts(s"if !(${translator.translate(checkExpr)}) {")
out.puts(s"if $failCondExpr {")
out.inc
val errInst = s"kaitai.New${err.name}($errArgsStr)"
val noValueAndErr = translator.returnRes match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -784,9 +784,25 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)
checkExpr: Ast.expr,
err: KSError,
errArgs: List[Ast.expr]
): Unit =
attrValidate(s"!(${translator.translate(checkExpr)})", err, errArgs)

override def attrValidateInEnum(
attrId: Identifier,
et: EnumType,
valueExpr: Ast.expr,
err: ValidationNotInEnumError,
errArgs: List[Ast.expr]
): Unit = {
// NOTE: this condition works for now because we haven't implemented
// https://github.com/kaitai-io/kaitai_struct/issues/778 for Java yet, but
// it will need to be changed when we do.
attrValidate(s"${translator.translate(valueExpr)} == null", err, errArgs)
}

private def attrValidate(failCondExpr: String, err: KSError, errArgs: List[Ast.expr]): Unit = {
val errArgsStr = errArgs.map(translator.translate).mkString(", ")
out.puts(s"if (!(${translator.translate(checkExpr)})) {")
out.puts(s"if ($failCondExpr) {")
out.inc
out.puts(s"throw new ${ksErrorName(err)}($errArgsStr);")
out.dec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -586,9 +586,24 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)
checkExpr: Ast.expr,
err: KSError,
errArgs: List[Ast.expr]
): Unit =
attrValidate(s"!(${translator.translate(checkExpr)})", attrId, err, errArgs)

override def attrValidateInEnum(
attrId: Identifier,
et: EnumType,
valueExpr: Ast.expr,
err: ValidationNotInEnumError,
errArgs: List[Ast.expr]
): Unit = {
val enumSpec = et.enumSpec.get
val enumRef = types2class(enumSpec.name, enumSpec.isExternal(typeProvider.nowClass))
attrValidate(s"!Object.prototype.hasOwnProperty.call($enumRef, ${translator.translate(valueExpr)})", attrId, err, errArgs)
}

private def attrValidate(failCondExpr: String, attrId: Identifier, err: KSError, errArgs: List[Ast.expr]): Unit = {
val errArgsStr = errArgs.map(translator.translate).mkString(", ")
out.puts(s"if (!(${translator.translate(checkExpr)})) {")
out.puts(s"if ($failCondExpr) {")
out.inc
val errObj = s"new ${ksErrorName(err)}($errArgsStr)"
if (attrDebugNeeded(attrId)) {
Expand Down
20 changes: 18 additions & 2 deletions shared/src/main/scala/io/kaitai/struct/languages/LuaCompiler.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.kaitai.struct.languages

import io.kaitai.struct.{ClassTypeProvider, RuntimeConfig, Utils, ExternalType}
import io.kaitai.struct.datatype.{DataType, FixedEndian, InheritedEndian, KSError, ValidationNotEqualError}
import io.kaitai.struct.datatype.{DataType, FixedEndian, InheritedEndian, KSError, ValidationNotEqualError, ValidationNotInEnumError}
import io.kaitai.struct.datatype.DataType._
import io.kaitai.struct.exprlang.Ast
import io.kaitai.struct.format._
Expand Down Expand Up @@ -426,9 +426,25 @@ class LuaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)
checkExpr: Ast.expr,
err: KSError,
errArgs: List[Ast.expr]
): Unit =
attrValidate(s"not(${translator.translate(checkExpr)})", err, errArgs)

override def attrValidateInEnum(
attrId: Identifier,
et: EnumType,
valueExpr: Ast.expr,
err: ValidationNotInEnumError,
errArgs: List[Ast.expr]
): Unit = {
// NOTE: this condition works for now because we haven't implemented
// https://github.com/kaitai-io/kaitai_struct/issues/778 for Lua yet, but
// it will need to be changed when we do.
attrValidate(s"${translator.translate(valueExpr)} == nil", err, errArgs)
}

private def attrValidate(failCondExpr: String, err: KSError, errArgs: List[Ast.expr]): Unit = {
val errArgsCode = errArgs.map(translator.translate)
out.puts(s"if not(${translator.translate(checkExpr)}) then")
out.puts(s"if $failCondExpr then")
out.inc
val msg = err match {
case _: ValidationNotEqualError => {
Expand Down
28 changes: 26 additions & 2 deletions shared/src/main/scala/io/kaitai/struct/languages/PHPCompiler.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.kaitai.struct.languages

import io.kaitai.struct.datatype.DataType._
import io.kaitai.struct.datatype.{CalcEndian, DataType, FixedEndian, InheritedEndian, KSError, UndecidedEndiannessError}
import io.kaitai.struct.datatype.{CalcEndian, DataType, FixedEndian, InheritedEndian, KSError, UndecidedEndiannessError, ValidationNotInEnumError}
import io.kaitai.struct.exprlang.Ast
import io.kaitai.struct.format.{NoRepeat, RepeatEos, RepeatExpr, RepeatSpec, _}
import io.kaitai.struct.languages.components._
Expand Down Expand Up @@ -439,6 +439,15 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)
universalDoc(label.doc)
out.puts(s"const ${value2Const(label.name)} = ${translator.doIntLiteral(id)};")
}
out.puts
val arrayEntriesStr = enumColl.map { case (id, _) => s"$id => true" }.mkString(", ")
out.puts(s"private const _VALUES = [$arrayEntriesStr];")
out.puts
out.puts("public static function isDefined(int $v): bool {")
out.inc
out.puts("return isset(self::_VALUES[$v]);")
out.dec
out.puts("}")
classFooter(name)
}

Expand Down Expand Up @@ -500,9 +509,24 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)
checkExpr: Ast.expr,
err: KSError,
errArgs: List[Ast.expr]
): Unit =
attrValidate(s"!(${translator.translate(checkExpr)})", err, errArgs)

override def attrValidateInEnum(
attrId: Identifier,
et: EnumType,
valueExpr: Ast.expr,
err: ValidationNotInEnumError,
errArgs: List[Ast.expr]
): Unit = {
val enumSpec = et.enumSpec.get
val enumRef = translator.types2classAbs(enumSpec.name)
attrValidate(s"!$enumRef::isDefined(${translator.translate(valueExpr)})", err, errArgs)
}

private def attrValidate(failCondExpr: String, err: KSError, errArgs: List[Ast.expr]): Unit = {
val errArgsStr = errArgs.map(translator.translate).mkString(", ")
out.puts(s"if (!(${translator.translate(checkExpr)})) {")
out.puts(s"if ($failCondExpr) {")
out.inc
out.puts(s"throw new ${ksErrorName(err)}($errArgsStr);")
out.dec
Expand Down
Loading

0 comments on commit 793ba60

Please sign in to comment.