我必须实现一些专有的二进制格式,并希望使用scodec执行此操作。但是,我找不到简洁的解决方案。 格式如下:一个文件由多个记录组成,其中每个记录都带有一个小字节16位数字" t"(uint16L)。 记录可以分为4类,具体取决于t的第一个和第二个字节的值:
如果t无效,程序应退出,因为文件已损坏。 如果t为Normal或Bar,则Record的长度为32位little endian int。 如果t是Foo,则必须解析另一个16位 big endian 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"))
此外,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:我发布了另一个解决方案,如下面的答案
答案 0 :(得分:2)
我将此作为答案发布,因为我对此解决方案感到满意,尽管这可能是我的第一个解决方案(问题中的edit1)与此解决方案之间的个人偏好问题。如果想要跟踪鉴别器值(我宁愿不这样做),Shastick的答案也提供了一种有用的方法。
我希望这对其他人也有帮助。
以下是解决方案2:我不是使用预定义的编解码器,而是单独解码和编码。
解码选择正确的编解码器而不多次解码类型,而编码从Recordtype中推导出正确的类型值(Bar / Foo -Records具有常量类型,而NormalRecords由Record.normalCodec中的DiscriminatorCodec编码)
trait KnownRecord
sealed trait NormalRecord extends KnownRecord
case class BarRecord(..,) extends KnownRecord
object BarRecord {
val codec: Codec[BarRecord] = {
//...
}.as[BarRecord]
}
case class FooRecord(...) extends KnownRecord
object FooRecord {
val codec: Codec[FooRecord] = // ...
}
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" | uint16L) ::
(("Length" | int32L) >>:~ { length =>
("Data" | bytes(length - 6)).hlist
})
}.as[UnknownRecord]
}
sealed trait ContainerType
object ContainerType{
case object FooType extends ContainerType
case object BarType extends ContainerType
case class NormalType(rtype: Int) extends ContainerType
case class Invalid(rtype: Int) extends ContainerType
implicit val codec: Codec[ContainerType] = {
def from(value: Int): ContainerType = {
val first = value & 0xFF
val second = (value >> 8) & 0xFF
value match {
case 0x0504 => FooType
case 0x08FF => BarType
case i if (second == 0 && first != 0) => NormalType(i)
case other => Invalid(other)
}
}
def to(ct: ContainerType): Int = ct match {
case FooType => 0x0302
case BarType => 0x0FFF
case NormalType(i) => i
case Invalid(i) => i
}
uint16L.xmap(from, to)
}
}
object Record{
type Record = Either[UnknownRecord, KnownRecord]
val ensureSize = new CodecTransformation {
def apply[X](c: Codec[X]) = variableSizeBytes(int32L, c.complete,
sizePadding=6)
}
val normalCodec: Codec[NormalRecord] =
normalCodec(uint16L).framing(ensureSize).as[NormalRecord]
def normalCodec(discr: Codec[Int]) =
discriminated[NormalRecord].by(discr)
.typecase(1, A.codec)
.typecase(2, B.codec)
.typecase(3, C.codec)
.typecase(4, D.codec)
val knownCodec: Codec[KnownRecord] = {
import ContainerType._
def decodeRecord(bits: BitVector): Attempt[DecodeResult[KnownRecord]] =
for {
ct <- ContainerType.codec.decode(bits)
rec <- ct.value match {
case FooType => FooRecord.codec.decode(ct.remainder)
case BarType =>
ensureSize(BarRecord.codec).decode(ct.remainder)
case NormalType(i) =>
ensureSize(normalCodec(provide(i))).decode(ct.remainder)
case Invalid(rtype) =>
Attempt.failure(Err(s"Invalid Type: $rtype!"))
}
} yield rec
def encodeRecord(rec: KnownRecord): Attempt[BitVector] =
rec match {
case c: NormalRecord => normalCodec.encode(c)
case fr: FooRecord => for {
rtype <- ContainerType.codec.encode(FooType)
record <- FooRecord.codec.encode(fr)
} yield rtype ++ record
case br: BarRecord => for {
rtype <- ContainerType.codec.encode(BarType)
record <- BarRecord.codec.encode(br)
length <- int32L.encode((record.size / 8).toInt + 6)
} yield rtype ++ length ++ record
}
Codec(Encoder(encodeRecord _), Decoder(decodeRecord _))
}
implicit val codec: Codec[Record] = {
discriminatorFallback(UnknownRecord.codec, knownCodec)
}
答案 1 :(得分:0)
我不确定您使用DiscriminatorCodec
可能会做什么(如果我理解您想要跟踪定义ContainerType
的uint16的值)< / p>
选项可以从Codec[(Int, ContainerType)]
开始,并使用consume()
选择基于ContainerType
的正确编解码器。
这可能看起来像:
def typeCodec: Codec[(Int, ContainerType)] =
uint16L.xmap(t => (t, to(t)), tup => tup._1)
/** Codec for decoding/encoding RecordPrefixes */
def prefixCodec: Codec[RecordPrefix] =
typeCodec.consume(selectCodec)(_.typeTuple)
def selectCodec(tup: (Int, ContainerType)) =
tup match {
case (i, Normal) => // Return codec for normal, using 'i' if required
case ...
}
这需要将def typeTuple: (Int, ContainerType)
函数添加到RecordPrefix
才能进行编码。
然后,一旦你有了Codec[RecordPrefix]
,就可以用它来解析其余部分(使用consume()
,flatPrepend()
,...)
或者,您可以使用peek(uint16L) :: <DiscriminatorCodec>
来保持值,同时仍然使用鉴别器编解码器。在这种情况下,编码时要小心peek
。
希望有所帮助。