All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.kaitai.struct.format.ClassSpec.scala Maven / Gradle / Ivy

package io.kaitai.struct.format

import io.kaitai.struct.datatype.DataType
import io.kaitai.struct.datatype.DataType._
import io.kaitai.struct.exprlang.Ast
import io.kaitai.struct.problems.KSYParseError

import scala.collection.mutable

/**
  * Type that we use when we want to refer to a class specification or something
  * close, but not (yet) that well defined.
  */
sealed trait ClassSpecLike {
  def toDataType: DataType
}
case object UnknownClassSpec extends ClassSpecLike {
  override def toDataType: DataType = CalcKaitaiStructType
}
case object GenericStructClassSpec extends ClassSpecLike {
  override def toDataType: DataType = CalcKaitaiStructType
}

sealed trait Sized
case object DynamicSized extends Sized
case object NotCalculatedSized extends Sized
case object StartedCalculationSized extends Sized
case class FixedSized(n: Int) extends Sized

case class ClassSpec(
  fileName: Option[String],
  path: List[String],
  isTopLevel: Boolean,
  meta: MetaSpec,
  doc: DocSpec,
  toStringExpr: Option[Ast.expr],
  params: List[ParamDefSpec],
  seq: List[AttrSpec],
  types: Map[String, ClassSpec],
  instances: Map[InstanceIdentifier, InstanceSpec],
  enums: Map[String, EnumSpec]
) extends ClassSpecLike with YAMLPath {
  var parentClass: ClassSpecLike = UnknownClassSpec

  /**
    * Full absolute name of the class (including all names of classes that
    * it's nested into, as a namespace). Derived either from `meta`/`id`
    * (for top-level classes), or from keys in `types` (for nested classes).
    */
  var name = List[String]()

  /**
    * @return Absolute name of class as string, components separated by
    *         double colon operator `::`
    */
  def nameAsStr: String = name.mkString("::")

  /**
    * @return Name of the file this type originates from, or, worst case,
    *         if filename is unknown, name of the type.
    */
  def fileNameAsStr: String = fileName.getOrElse(nameAsStr)

  /**
    * The class specification that this class is nested into, if it exists.
    * For top-level classes, it's None.
    */
  var upClass: Option[ClassSpec] = None

  var seqSize: Sized = NotCalculatedSized

  def toDataType: DataType = {
    val cut = CalcUserType(name, None)
    cut.classSpec = Some(this)
    cut
  }

  def parentType: DataType = parentClass.toDataType

  /**
    * Recursively traverses tree of types starting from this type, calling
    * certain function for every type, starting from this one.
    *
    * @param proc function to execute on every encountered type.
    */
  def forEachRec(proc: (ClassSpec) => Unit): Unit = {
    proc.apply(this)
    types.foreach { case (_, typeSpec) =>
      typeSpec.forEachRec(proc)
    }
  }

  /**
    * Recursively traverses tree of types starting from this type, calling
    * certain function for every type, starting from this one.
    *
    * @param proc function to execute on every encountered type.
    * @tparam R mandates that function must return a list of this type.
    */
  def mapRec[R](proc: (ClassSpec) => Iterable[R]): Iterable[R] = {
    val r1 = proc.apply(this)
    val r2 = types.flatMap { case (_, typeSpec) =>
      typeSpec.mapRec(proc)
    }
    r1 ++ r2
  }

  override def equals(obj: Any): Boolean = obj match {
    case other: ClassSpec =>
      path == other.path &&
      isTopLevel == other.isTopLevel &&
      meta == other.meta &&
      doc == other.doc &&
      params == other.params &&
      seq == other.seq &&
      types == other.types &&
      instances == other.instances &&
      enums == other.enums &&
      name == other.name
    case _ => false
  }
}

object ClassSpec {
  val LEGAL_KEYS = Set(
    "meta",
    "doc",
    "doc-ref",
    "to-string",
    "params",
    "seq",
    "types",
    "instances",
    "enums"
  )

  def fromYaml(src: Any, fileName: Option[String], path: List[String], metaDef: MetaSpec): ClassSpec = {
    val srcMap = ParseUtils.asMapStr(src, path)
    ParseUtils.ensureLegalKeys(srcMap, LEGAL_KEYS, path)

    val metaPath = path ++ List("meta")
    val explicitMeta = srcMap.get("meta").map(MetaSpec.fromYaml(_, metaPath)).getOrElse(MetaSpec.emptyWithPath(metaPath))
    val meta = explicitMeta.fillInDefaults(metaDef)

    val doc = DocSpec.fromYaml(srcMap, path)

    val toStringExpr = ParseUtils.getOptValueExpression(srcMap, "to-string", path)

    val params: List[ParamDefSpec] = srcMap.get("params") match {
      case Some(value) => paramDefFromYaml(value, path ++ List("params"))
      case None => List()
    }
    val seq: List[AttrSpec] = srcMap.get("seq") match {
      case Some(value) => seqFromYaml(value, path ++ List("seq"), meta)
      case None => List()
    }
    val instances: Map[InstanceIdentifier, InstanceSpec] = srcMap.get("instances") match {
      case Some(value) => instancesFromYaml(value, path ++ List("instances"), meta)
      case None => Map()
    }

    checkDupMemberIds(params ++ seq ++ instances.values)

    val types: Map[String, ClassSpec] = srcMap.get("types") match {
      case Some(value) => typesFromYaml(value, fileName, path ++ List("types"), meta)
      case None => Map()
    }
    val enums: Map[String, EnumSpec] = srcMap.get("enums") match {
      case Some(value) => enumsFromYaml(value, path ++ List("enums"))
      case None => Map()
    }

    val cs = ClassSpec(
      fileName, path, path.isEmpty,
      meta, doc, toStringExpr,
      params, seq, types, instances, enums
    )

    // If that's a top-level class, set its name from meta/id
    if (path.isEmpty) {
      explicitMeta.id match {
        case None =>
          throw KSYParseError.withText("no `meta/id` encountered in top-level class spec", path ++ List("meta", "id"))
        case Some(id) =>
          cs.name = List(id)
      }
    }

    cs
  }

  def paramDefFromYaml(src: Any, path: List[String]): List[ParamDefSpec] = {
    src match {
      case srcList: List[Any] =>
        val params = srcList.zipWithIndex.map { case (attrSrc, idx) =>
          ParamDefSpec.fromYaml(attrSrc, path ++ List(idx.toString), idx)
        }
        params
      case unknown =>
        throw KSYParseError.withText(s"expected array, found $unknown", path)
    }
  }

  def seqFromYaml(src: Any, path: List[String], metaDef: MetaSpec): List[AttrSpec] = {
    src match {
      case srcList: List[Any] =>
        val seq = srcList.zipWithIndex.map { case (attrSrc, idx) =>
          AttrSpec.fromYaml(attrSrc, path ++ List(idx.toString), metaDef, idx)
        }
        seq
      case unknown =>
        throw KSYParseError.withText(s"expected array, found $unknown", path)
    }
  }

  def checkDupMemberIds(attrs: List[MemberSpec]): Unit = {
    val attrIds = mutable.Map[String, MemberSpec]()
    attrs.foreach { (attr) =>
      val idOpt: Option[String] = attr.id match {
        case NamedIdentifier(name) => Some(name)
        case InstanceIdentifier(name) => Some(name)
        case _ => None // do nothing with non-named IDs
      }
      idOpt.foreach { (id) =>
        checkDupId(attrIds.get(id), id, attr)
        attrIds.put(id, attr)
      }
    }
  }

  private def checkDupId(prevAttrOpt: Option[MemberSpec], id: String, nowAttr: YAMLPath) {
    prevAttrOpt match {
      case Some(prevAttr) =>
        throw KSYParseError.withText(
          s"duplicate attribute ID '$id', previously defined at /${prevAttr.pathStr}",
          nowAttr.path
        )
      case None =>
        // no dups, ok
    }
  }

  def typesFromYaml(src: Any, fileName: Option[String], path: List[String], metaDef: MetaSpec): Map[String, ClassSpec] = {
    val srcMap = ParseUtils.asMapStr(src, path)
    srcMap.map { case (typeName, body) =>
      Identifier.checkIdentifierSource(typeName, "type", path ++ List(typeName))
      typeName -> ClassSpec.fromYaml(body, fileName, path ++ List(typeName), metaDef)
    }
  }

  def instancesFromYaml(src: Any, path: List[String], metaDef: MetaSpec): Map[InstanceIdentifier, InstanceSpec] = {
    val srcMap = ParseUtils.asMap(src, path)
    srcMap.map { case (key, body) =>
      val instName = ParseUtils.asStr(key, path)
      Identifier.checkIdentifierSource(instName, "instance", path ++ List(instName))
      val id = InstanceIdentifier(instName)
      id -> InstanceSpec.fromYaml(body, path ++ List(instName), metaDef, id)
    }
  }

  def enumsFromYaml(src: Any, path: List[String]): Map[String, EnumSpec] = {
    val srcMap = ParseUtils.asMap(src, path)
    srcMap.map { case (key, body) =>
      val enumName = ParseUtils.asStr(key, path)
      Identifier.checkIdentifierSource(enumName, "enum", path ++ List(enumName))
      enumName -> EnumSpec.fromYaml(body, path ++ List(enumName))
    }
  }

  def fromYaml(src: Any, fileName: Option[String]): ClassSpec = fromYaml(src, fileName, List(), MetaSpec.OPAQUE)

  def opaquePlaceholder(typeName: List[String]): ClassSpec = {
    val placeholder = ClassSpec(
      fileName = None,
      path = List(),
      isTopLevel = true,
      meta = MetaSpec.OPAQUE,
      doc = DocSpec.EMPTY,
      toStringExpr = None,
      params = List(),
      seq = List(),
      types = Map(),
      instances = Map(),
      enums = Map()
    )
    placeholder.name = typeName
    placeholder
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy