我必须实现一些专有的二进制格式,并想用 scodec 来做到这一点。但是,我找不到简洁的解决方案。格式如下: 一个文件由多个 Records 组成,其中每个 Record 都以一个 little endian 16 位数字“t”(uint16L) 为前缀。根据 t 的第一个和第二个字节的值,记录可以分为 4 类:
- 正常:t.first != 0 && t.second == 0
- 条形图:t.first == 0x08 && t.second == 0xFF
- Foo: t.first == 0x04 && t.second == 0x05
- 无效:t 不是以上任何一项
如果 t 无效,则程序应退出,因为文件已损坏。如果 t 是 Normal 或 Bar,则 Record 的长度遵循 32 位 little endian int。如果 t 为 Foo,则必须先解析另一个 16 位大端int,然后才能将长度解析为 32 位 BE int。
- Normal: ("t" | uint16L) :: ("length" | uint32L) :: [Record data discriminated by t]
- Bar: ("t" | constant(0x08FF)) :: ("length" | uint32L) :: [Record data of Bar]
- Foo: ("t" | constant(0x0405)) :: uint16 :: ("length" | uint32) :: [Record data of foo]
- Invalid: ("t" | uint16L ) :: fail(Err(s"invalid type: $t"))
此外,“Normal”中 t 的某些值未使用,应生成 UnknownRecord(类似于此处的 mpeg 实现:https ://github.com/scodec/scodec-protocols/blob/series/1.0.x/src/main /scala/scodec/protocols/mpeg/Descriptor.scala )
这是我目前的方法,但感觉不太清楚,我觉得我更多地围绕 scodec 工作,而不是使用它。有任何想法吗?随意在下面废弃我的代码..
sealed trait ContainerType
object ContainerType{
implicit class SplitInt(val self: Int) extends AnyVal{
def first = self & 0xFF
def second = (self >> 8) & 0xFF
}
case object Normal extends ContainerType
case object Bar extends ContainerType
case object Foo extends ContainerType
case object Invalid extends ContainerType
val codec: Codec[ContainerType] = {
def to(value: Int): ContainerType = value match{
case v if value.first != 0 && value.second == 0 => Normal
case v if value.first == 0x08 && value.second == 0xFF => Bar
case v if value.first == 0x04 && value.second == 0x05 => Foo
case _ => Invalid
}
uint16L.xmap(to, ??) // don't have value here
// if I use case classes and save the value I can't discriminate by it in RecordPrefix
}
}
sealed trait RecordPrefix{
def t : Int,
def length: Int
}
object RecordPrefix {
case class Normal( override val t: Int, override val length: Int) extends RecordPrefix
object Normal{
val codec: Codec[Normal] = ??
}
case class Bar(override val t: Int, override val length: Int) extends RecordPrefix
object Bar{
val codec: Codec[Bar] = ??
}
case class Foo(override val t: Int, foobar: Int, length: Int) extends RecordPrefix
object Foo{
val codec: Codec[Foo] = ??
}
val codec: Codec[RecordPrefix] = {
discriminated[RecordPrefix].by(ContainerType.codec)
.typecase(Normal, Normal.codec)
.typecase(Bar, Bar.codec)
.typecase(Foo, Foo.codec)
// how to handle invalid case ?
}
}
case class Record(prefix: RecordPrefix, body: RecordBody)
sealed trait RecordBody
//.... How can I implement the codecs?
PS:这是我在这里的第一个问题,我希望它足够清楚。=)
Edit1:我找到了一个至少可以完成这项工作的实现。如果记录未知,我会权衡以再次检查条件,以获得更清晰的层次结构。
trait KnownRecord
sealed trait NormalRecord extends KnownRecord
case class BarRecord(length: Int, ..,) extends KnownRecord
object BarRecord {
val codec: Codec[BarRecord] = {
("Length" | int32L) ::
//...
}.as[BarRecord]
}
case class FooRecord(...) extends KnownRecord
object FooRecord {
val codec: Codec[FooRecord] = // analogue
}
case class A() extends NormalRecord
case class B() extends NormalRecord
// ...
case class UnknownRecord(rtype: Int, length: Int, data: ByteVector)
object UnknownRecord{
val codec: Codec[UnknownRecord] = {
("Type" | Record.validTypeCodec) ::
(("Length" | int32L) >>:~ { length =>
("Data" | bytes(length - 6)).hlist
})
}.as[UnknownRecord]
}
object Record{
type Record = Either[UnknownRecord, KnownRecord]
val validTypeCodec: Codec[Int] = {
uint16L.consume[Int] { rtype =>
val first = rtype & 0xFF
val second = (rtype >> 8) & 0xFF
rtype match {
case i if first != 0 && second == 0 => provide(i)
case i if first == 0x04 && second == 0x05 => provide(i)
case i if first == 0xFF && second == 0x08 => provide(i)
case _ => fail(Err(s"Invalid Type: $rtype!"))
}
} (identity)
}
def normalCodec(rtype: Int): Codec[NormalRecord] = {
discriminated[NormalRecord].by(provide(rtype))
.typecase(1, A.codec)
.typecase(2, B.codec)
.typecase(3, C.codec)
.typecase(4, D.codec)
.framing(new CodecTransformation {
def apply[X](c: Codec[X]) = variableSizeBytes(int32L, c.complete,
sizePadding=6)
})
}.as[NormalRecord]
val knownCodec: Codec[KnownRecord] = {
val b = discriminated[KnownRecord].by(("Type" | uint16L))
.typecase(0x0504, FooRecord.codec)
.typecase(0x08FF, BarRecord.codec)
(1 to 0xFF).foldLeft(b) {
(acc, x) => acc.typecase(x, normalCodec(x))
}
}
implicit val codec: Codec[Record] = {
discriminatorFallback(UnknownRecord.codec, knownCodec)
}
Edit2:我在下面发布了一个替代解决方案作为答案