diff --git a/shared/src/main/scala/io/kaitai/struct/GraphvizClassCompiler.scala b/shared/src/main/scala/io/kaitai/struct/GraphvizClassCompiler.scala index 22c746953..7cd1d29a8 100644 --- a/shared/src/main/scala/io/kaitai/struct/GraphvizClassCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/GraphvizClassCompiler.scala @@ -182,8 +182,8 @@ class GraphvizClassCompiler(classSpecs: ClassSpecs, topClass: ClassSpec) extends val text = valid match { case Some(v) => v match { - case ValidationEq(expected) => - s"must be equal to ${expression(expected, fullPortName, STYLE_EDGE_VALID)}" + case expected: ValidationEquals => + s"must be equal to ${expression(expected.value, fullPortName, STYLE_EDGE_VALID)}" case ValidationMin(min) => s"must be at least ${expression(min, fullPortName, STYLE_EDGE_VALID)}" case ValidationMax(max) => @@ -517,12 +517,17 @@ object GraphvizClassCompiler extends LanguageCompilerStatic { private def fixedBytes(blt: BytesLimitType, valid: Option[ValidationSpec]): Option[String] = { valid match { - case Some(ValidationEq(Ast.expr.List(contents))) - if blt.size == Ast.expr.IntNum(contents.length) => - Some(contents.map(_ match { - case Ast.expr.IntNum(byteVal) if byteVal >= 0x00 && byteVal <= 0xff => "%02X".format(byteVal) - case _ => return None - }).mkString(" ")) + case Some(expected: ValidationEquals) => { + expected.value match { + case Ast.expr.List(contents) if blt.size == Ast.expr.IntNum(contents.length) => { + Some(contents.map(_ match { + case Ast.expr.IntNum(byteVal) if byteVal >= 0x00 && byteVal <= 0xff => "%02X".format(byteVal) + case _ => return None + }).mkString(" ")) + } + case _ => None + } + } case _ => None } } diff --git a/shared/src/main/scala/io/kaitai/struct/format/ValidationSpec.scala b/shared/src/main/scala/io/kaitai/struct/format/ValidationSpec.scala index 3f404ce4f..dab00e96b 100644 --- a/shared/src/main/scala/io/kaitai/struct/format/ValidationSpec.scala +++ b/shared/src/main/scala/io/kaitai/struct/format/ValidationSpec.scala @@ -5,7 +5,14 @@ import io.kaitai.struct.problems.KSYParseError sealed trait ValidationSpec -case class ValidationEq(value: Ast.expr) extends ValidationSpec +sealed abstract class ValidationEquals(val value: Ast.expr) extends ValidationSpec +/** `valid: <...>` key */ +case class Validation(_value: Ast.expr) extends ValidationEquals(_value) +/** `valid: eq: <...>` key */ +case class ValidationEq(_value: Ast.expr) extends ValidationEquals(_value) +/** `contents: <...>` key */ +case class ValidationContents(_value: Ast.expr) extends ValidationEquals(_value) + 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 @@ -13,10 +20,10 @@ case class ValidationAnyOf(values: List[Ast.expr]) extends ValidationSpec case class ValidationInEnum() extends ValidationSpec case class ValidationExpr(checkExpr: Ast.expr) extends ValidationSpec -object ValidationEq { +object ValidationEquals { val LEGAL_KEYS = Set("eq") - def fromMap(src: Map[String, Any], path: List[String]): Option[ValidationEq] = + def fromMap(src: Map[String, Any], path: List[String]): Option[ValidationEquals] = ParseUtils.getOptValueExpression(src, "eq", path).map { case eqExpr => ParseUtils.ensureLegalKeys(src, LEGAL_KEYS, path) ValidationEq(eqExpr) @@ -89,7 +96,7 @@ object ValidationExpr { object ValidationSpec { val LEGAL_KEYS = - ValidationEq.LEGAL_KEYS ++ + ValidationEquals.LEGAL_KEYS ++ ValidationRange.LEGAL_KEYS ++ ValidationAnyOf.LEGAL_KEYS ++ ValidationInEnum.LEGAL_KEYS ++ @@ -113,10 +120,10 @@ object ValidationSpec { } def fromString(value: String, path: List[String]): ValidationSpec = - ValidationEq(Expressions.parse(value)) + Validation(Expressions.parse(value)) def fromMap(src: Map[String, Any], path: List[String]): ValidationSpec = { - val opt1 = ValidationEq.fromMap(src, path) + val opt1 = ValidationEquals.fromMap(src, path) if (opt1.nonEmpty) return opt1.get val opt2 = ValidationRange.fromMap(src, path) diff --git a/shared/src/main/scala/io/kaitai/struct/languages/components/ValidateOps.scala b/shared/src/main/scala/io/kaitai/struct/languages/components/ValidateOps.scala index 2c346726b..f20121cb2 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/components/ValidateOps.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/components/ValidateOps.scala @@ -17,8 +17,8 @@ trait ValidateOps extends ExceptionNames { def attrValidate(attr: AttrLikeSpec, valid: ValidationSpec): Unit = { val itemValue = Identifier.itemExpr(attr.id, attr.cond.repeat) valid match { - case ValidationEq(expected) => - attrValidateExprCompare(attr, Ast.cmpop.Eq, expected, ValidationNotEqualError(attr.dataType)) + case expected: ValidationEquals => + attrValidateExprCompare(attr, Ast.cmpop.Eq, expected.value, ValidationNotEqualError(attr.dataType)) case ValidationMin(min) => attrValidateExprCompare(attr, Ast.cmpop.GtE, min, ValidationLessThanError(attr.dataType)) case ValidationMax(max) => diff --git a/shared/src/main/scala/io/kaitai/struct/precompile/TypeValidator.scala b/shared/src/main/scala/io/kaitai/struct/precompile/TypeValidator.scala index d80741484..dbfd60b36 100644 --- a/shared/src/main/scala/io/kaitai/struct/precompile/TypeValidator.scala +++ b/shared/src/main/scala/io/kaitai/struct/precompile/TypeValidator.scala @@ -82,7 +82,12 @@ class TypeValidator(specs: ClassSpecs) extends PrecompileStep { val problemsDataType = validateDataType(attr.dataType, path) - List(problemsIf, problemsRepeat, problemsDataType).flatten + val problemsValid: Iterable[CompilationProblem] = attr.valid match { + case Some(valid) => validateValidClause(valid, attr.dataType, attr.path) + case None => None // all good + } + + List(problemsIf, problemsRepeat, problemsDataType, problemsValid).flatten } def validateParseInstance(pis: ParseInstanceSpec): Iterable[CompilationProblem] = { @@ -100,7 +105,12 @@ class TypeValidator(specs: ClassSpecs) extends PrecompileStep { case None => None // all good } - List(problemsAttr, problemsIo, problemsPos).flatten + val problemsValid: Iterable[CompilationProblem] = pis.valid match { + case Some(valid) => validateValidClause(valid, pis.dataType, pis.path) + case None => None // all good + } + + List(problemsAttr, problemsIo, problemsPos, problemsValid).flatten } def validateValueInstance(vis: ValueInstanceSpec): Option[CompilationProblem] = { @@ -204,6 +214,46 @@ class TypeValidator(specs: ClassSpecs) extends PrecompileStep { } } + /** + * @param valid The `valid` clause of Kaitai Struct attribute or parse instance definition + * @param attrType The type of attribute, used to check compatibility of valid expressions to that type + * @param path Path where `valid` key is defined + */ + private def validateValidClause( + valid: ValidationSpec, + attrType: DataType, + path: List[String] + ): Iterable[CompilationProblem] = { + // TODO: This is not user-face message + val expected = attrType.toString + val validPath = path :+ "valid" + valid match { + case Validation(value) => + checkAssertObject(value, attrType, expected, path, "valid") + case ValidationEq(value) => + checkAssertObject(value, attrType, expected, validPath, "eq") + case ValidationContents(value) => + checkAssertObject(value, attrType, expected, path, "contents") + case ValidationMin(min) => + checkAssertObject(min, attrType, expected, validPath, "min") + case ValidationMax(max) => + checkAssertObject(max, attrType, expected, validPath, "max") + case ValidationRange(min, max) => { + checkAssertObject(min, attrType, expected, validPath, "min") ++ + checkAssertObject(max, attrType, expected, validPath, "max") + } + case ValidationAnyOf(values) => { + val itemPath = validPath :+ "any-of" + values.zipWithIndex.flatMap { case (value, i) => + checkAssertObject(value, attrType, expected, itemPath, i.toString) + } + } + case ValidationInEnum() => List() + case ValidationExpr(checkExpr) => + checkAssert[BooleanType](checkExpr, "boolean", validPath, "expr") + } + } + /** * Checks that expression's type conforms to a given datatype, otherwise * throw a human-readable exception, with some pointers that would help