diff --git a/fmp/fmp_chunk.go b/fmp/fmp_chunk.go new file mode 100644 index 0000000..e1fa560 --- /dev/null +++ b/fmp/fmp_chunk.go @@ -0,0 +1,245 @@ +package fmp + +import ( + "encoding/binary" + "fmt" +) + +type FmpChunk struct { + Type FmpChunkType + Length uint32 + Key uint32 // If Type == FMP_CHUNK_SHORT_KEY_VALUE or FMP_CHUNK_LONG_KEY_VALUE + Index uint32 // Segment index, if Type == FMP_CHUNK_SEGMENTED_DATA + Value []byte +} + +func (c *FmpChunk) String() string { + return fmt.Sprintf("<%v(%v)>", c.Type, c.Length) +} + +func (ctx *FmpFile) readChunk(payload []byte) (*FmpChunk, error) { + + // Simple data + + if payload[0] == 0x00 || payload[0] == 0x19 || payload[0] == 0x23 { + return &FmpChunk{ + Type: FMP_CHUNK_SIMPLE_DATA, + Value: payload[1 : 1+1], + Length: 2, + }, nil + } + if payload[0] == 0x08 { + return &FmpChunk{ + Type: FMP_CHUNK_SIMPLE_DATA, + Value: payload[1 : 1+2], + Length: 3, + }, nil + } + if payload[0] == 0x0E && payload[1] == 0xFF { + return &FmpChunk{ + Type: FMP_CHUNK_SIMPLE_DATA, + Value: payload[2 : 2+5], + Length: 7, + }, nil + } + if 0x10 <= payload[0] && payload[0] <= 0x11 { + length := 3 + (payload[0] - 0x10) + return &FmpChunk{ + Type: FMP_CHUNK_SIMPLE_DATA, + Value: payload[1 : 1+length], + Length: 1 + uint32(length), + }, nil + } + if 0x12 <= payload[0] && payload[0] <= 0x15 { + length := 1 + 2*(payload[0]-0x10) + return &FmpChunk{ + Type: FMP_CHUNK_SIMPLE_DATA, + Value: payload[1 : 1+length], + Length: 1 + uint32(length), + }, nil + } + if 0x1A <= payload[0] && payload[0] <= 0x1D { + length := 2 * (payload[0] - 0x19) + return &FmpChunk{ + Type: FMP_CHUNK_SIMPLE_DATA, + Value: payload[1 : 1+length], + Length: 1 + uint32(length), + }, nil + } + + // Simple key-value + + if payload[0] == 0x01 { + return &FmpChunk{ + Type: FMP_CHUNK_SIMPLE_KEY_VALUE, + Key: uint32(payload[1]), + Value: payload[2 : 2+1], + Length: 3, + }, nil + } + if 0x02 <= payload[0] && payload[0] <= 0x05 { + length := 2 * (payload[0] - 1) + return &FmpChunk{ + Type: FMP_CHUNK_SIMPLE_KEY_VALUE, + Key: uint32(payload[1]), + Value: payload[2 : 2+length], + Length: 2 + uint32(length), + }, nil + } + if payload[0] == 0x06 { + length := payload[2] + return &FmpChunk{ + Type: FMP_CHUNK_SIMPLE_KEY_VALUE, + Key: uint32(payload[1]), + Value: payload[3 : 3+length], // docs say offset 2? + Length: 3 + uint32(length), + }, nil + } + if payload[0] == 0x09 { + return &FmpChunk{ + Type: FMP_CHUNK_SIMPLE_KEY_VALUE, + Key: binary.BigEndian.Uint32(payload[1:3]), + Value: payload[3 : 3+1], // docs say offset 2? + Length: 4, + }, nil + } + if 0x0A <= payload[0] && payload[0] <= 0x0D { + length := 2 * (payload[0] - 9) + return &FmpChunk{ + Type: FMP_CHUNK_SIMPLE_KEY_VALUE, + Key: binary.BigEndian.Uint32(payload[1:3]), + Value: payload[3 : 3+length], // docs say offset 2? + Length: 2 + uint32(length), + }, nil + } + if payload[0] == 0x0E { + length := payload[2] + return &FmpChunk{ + Type: FMP_CHUNK_SIMPLE_KEY_VALUE, + Key: binary.BigEndian.Uint32(payload[1:3]), + Value: payload[4 : 4+length], // docs say offset 2? + Length: 4 + uint32(length), + }, nil + } + + // Long key-value + + if payload[0] == 0x16 { + length := payload[4] + return &FmpChunk{ + Type: FMP_CHUNK_LONG_KEY_VALUE, + Key: binary.BigEndian.Uint32(payload[1 : 1+3]), + Value: payload[5 : 5+length], + Length: 5 + uint32(length), + }, nil + } + if payload[0] == 0x17 { + length := binary.BigEndian.Uint32(payload[1 : 1+3]) + return &FmpChunk{ + Type: FMP_CHUNK_LONG_KEY_VALUE, + Key: binary.BigEndian.Uint32(payload[4 : 4+2]), + Value: payload[6 : 6+length], + Length: 6 + uint32(length), + }, nil + } + if payload[0] == 0x1E { + keyLength := payload[1] + valueLength := payload[2+keyLength] + return &FmpChunk{ + Type: FMP_CHUNK_LONG_KEY_VALUE, + Key: binary.BigEndian.Uint32(payload[2 : 2+keyLength]), + Value: payload[2+keyLength+1 : 2+keyLength+1+valueLength], + Length: 3 + uint32(keyLength) + uint32(valueLength), + }, nil + } + if payload[0] == 0x1F { + keyLength := uint32(payload[1]) + valueLength := binary.BigEndian.Uint32(payload[2+keyLength : 2+keyLength+2+1]) + return &FmpChunk{ + Type: FMP_CHUNK_LONG_KEY_VALUE, + Key: binary.BigEndian.Uint32(payload[2 : 2+keyLength]), + Value: payload[2+keyLength+2 : 2+keyLength+2+valueLength], + Length: 4 + uint32(keyLength) + uint32(valueLength), + }, nil + } + + // Segmented data + + if payload[0] == 0x07 { + length := binary.BigEndian.Uint32(payload[2 : 2+2]) + return &FmpChunk{ + Type: FMP_CHUNK_SEGMENTED_DATA, + Index: uint32(payload[1]), + Value: payload[4 : 4+length], + Length: 4 + uint32(length), + }, nil + } + if payload[0] == 0x0F { + length := binary.BigEndian.Uint32(payload[3 : 3+2]) + return &FmpChunk{ + Type: FMP_CHUNK_SEGMENTED_DATA, + Index: binary.BigEndian.Uint32(payload[1 : 1+2]), + Value: payload[5 : 5+length], + Length: 5 + uint32(length), + }, nil + } + + // Path push + + if payload[0] == 0x20 || payload[0] == 0x0E { + if payload[1] == 0xFE { + return &FmpChunk{ + Type: FMP_CHUNK_PATH_PUSH, + Value: payload[1 : 1+8], + Length: 10, + }, nil + } + return &FmpChunk{ + Type: FMP_CHUNK_PATH_PUSH, + Value: payload[1 : 1+1], + Length: 2, + }, nil + } + if payload[0] == 0x28 { + return &FmpChunk{ + Type: FMP_CHUNK_PATH_PUSH, + Value: payload[1 : 1+2], + Length: 3, + }, nil + } + if payload[0] == 0x30 { + return &FmpChunk{ + Type: FMP_CHUNK_PATH_PUSH, + Value: payload[1 : 1+3], + Length: 4, + }, nil + } + if payload[0] == 0x38 { + length := payload[1] + return &FmpChunk{ + Type: FMP_CHUNK_PATH_PUSH, + Value: payload[2 : 2+length], + Length: 2 + uint32(length), + }, nil + } + + // Path pop + + if payload[0] == 0x3D && payload[1] == 0x40 { + return &FmpChunk{ + Type: FMP_CHUNK_PATH_POP, + Length: 1, + }, nil + } + + // No-op + + if payload[0] == 0x80 { + return &FmpChunk{ + Type: FMP_CHUNK_NOOP, + Length: 1, + }, nil + } + + return nil, nil +} diff --git a/fmp/fmp_const.go b/fmp/fmp_const.go index 4caa2e6..82426cd 100644 --- a/fmp/fmp_const.go +++ b/fmp/fmp_const.go @@ -1,5 +1,28 @@ package fmp +type FmpError string +type FmpChunkType uint8 + +func (e FmpError) Error() string { return string(e) } + +var ( + ErrRead = FmpError("read error") + ErrBadMagic = FmpError("bad magic number") + ErrBadHeader = FmpError("bad header") + ErrUnsupportedCharset = FmpError("unsupported character set") + ErrBadSectorCount = FmpError("bad sector count") +) + +const ( + FMP_CHUNK_SIMPLE_DATA FmpChunkType = iota + FMP_CHUNK_SEGMENTED_DATA + FMP_CHUNK_SIMPLE_KEY_VALUE + FMP_CHUNK_LONG_KEY_VALUE + FMP_CHUNK_PATH_PUSH + FMP_CHUNK_PATH_POP + FMP_CHUNK_NOOP +) + const ( FMP_COLLATION_ENGLISH = 0x00 FMP_COLLATION_FRENCH = 0x01 diff --git a/fmp/fmp_error.go b/fmp/fmp_error.go deleted file mode 100644 index 8b5b476..0000000 --- a/fmp/fmp_error.go +++ /dev/null @@ -1,18 +0,0 @@ -package fmp - -type FmpError string - -func (e FmpError) Error() string { return string(e) } - -var ( - ErrRead = FmpError("read error") - ErrBadMagic = FmpError("bad magic number") - ErrBadHeader = FmpError("bad header") - ErrUnsupportedCharset = FmpError("unsupported character set") - ErrBadSectorCount = FmpError("bad sector count") - ErrNoFmemopen = FmpError("no fmemopen") - ErrOpen = FmpError("could not open file") - ErrSeek = FmpError("seek failed") - ErrMalloc = FmpError("malloc failed") - ErrUserAborted = FmpError("user aborted") -) diff --git a/fmp/fmp.go b/fmp/fmp_file.go similarity index 54% rename from fmp/fmp.go rename to fmp/fmp_file.go index 670598d..5c0e23c 100644 --- a/fmp/fmp.go +++ b/fmp/fmp_file.go @@ -12,19 +12,19 @@ const ( magicSequence = "\x00\x01\x00\x00\x00\x02\x00\x01\x00\x05\x00\x02\x00\x02\xC0" hbamSequence = "HBAM7" - magicSize = len(magicSequence) - hbamSize = len(hbamSequence) - sectorSize = 4096 + magicSize = len(magicSequence) + hbamSize = len(hbamSequence) + sectorSize = 4096 + sectorHeaderSize = 20 ) type FmpFile struct { - Stream io.ReadSeeker - - FileSize uint - NumSectors uint - - VersionDate time.Time - ApplicationName string + VersionDate time.Time + CreatorName string + FileSize uint + NumSectors uint + Stream io.ReadSeeker + Sectors []*FmpSector } type FmpSector struct { @@ -32,7 +32,7 @@ type FmpSector struct { Level uint8 PrevSectorID uint32 NextSectorID uint32 - Payload []byte + Chunks []*FmpChunk } func OpenFile(path string) (*FmpFile, error) { @@ -40,20 +40,30 @@ func OpenFile(path string) (*FmpFile, error) { if err != nil { return nil, err } + stream, err := os.Open(path) if err != nil { - if stream != nil { - stream.Close() - } return nil, err } + defer stream.Close() + ctx := &FmpFile{Stream: stream} if err := ctx.readHeader(); err != nil { - stream.Close() return nil, err } + ctx.FileSize = uint(info.Size()) ctx.NumSectors = ctx.FileSize / sectorSize + ctx.Sectors = make([]*FmpSector, ctx.NumSectors) + + for i := uint(0); i < ctx.NumSectors; i++ { + sector, err := ctx.readSector() + if err != nil { + return nil, err + } + ctx.Sectors[i] = sector + } + return ctx, nil } @@ -61,7 +71,7 @@ func (ctx *FmpFile) readHeader() error { buf := make([]byte, sectorSize) _, err := ctx.Stream.Read(buf) if err != nil { - return ErrRead + return err } if !bytes.Equal(buf[:magicSize], []byte(magicSequence)) { return ErrBadMagic @@ -76,23 +86,57 @@ func (ctx *FmpFile) readHeader() error { } appNameLength := int(buf[541]) - ctx.ApplicationName = string(buf[542 : 542+appNameLength]) + ctx.CreatorName = string(buf[542 : 542+appNameLength]) return nil } func (ctx *FmpFile) readSector() (*FmpSector, error) { - buf := make([]byte, sectorSize) - _, err := ctx.Stream.Read(buf) + buf := make([]byte, sectorHeaderSize) + n, err := ctx.Stream.Read(buf) + + if n == 0 { + return nil, io.EOF + } if err != nil { return nil, ErrRead } + sector := &FmpSector{ Deleted: buf[0] != 0, Level: uint8(buf[1]), PrevSectorID: binary.BigEndian.Uint32(buf[2:6]), NextSectorID: binary.BigEndian.Uint32(buf[6:10]), - Payload: buf[6:4076], } + + payload := make([]byte, sectorSize-sectorHeaderSize) + n, err = ctx.Stream.Read(payload) + if n != sectorSize-sectorHeaderSize { + return nil, ErrRead + } + if err != nil { + return nil, ErrRead + } + sector.Chunks = make([]*FmpChunk, 0) + + for { + chunk, err := ctx.readChunk(payload) + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + sector.Chunks = append(sector.Chunks, chunk) + if chunk == nil { + break + } + if chunk.Length == 0 { + panic("chunk length not set") + } + print(chunk.String() + "\n") + payload = payload[chunk.Length:] + } + return sector, nil } diff --git a/fmp/fmp_test.go b/fmp/fmp_test.go index 954d4bd..42086fc 100644 --- a/fmp/fmp_test.go +++ b/fmp/fmp_test.go @@ -13,10 +13,11 @@ func TestOpenFile(t *testing.T) { if f.NumSectors != 96 { t.Errorf("expected number of sectors to be 96, got %d", f.NumSectors) } - if f.ApplicationName != "Pro 12.0" { - t.Errorf("expected application name to be 'Pro 12.0', got '%s'", f.ApplicationName) + if f.CreatorName != "Pro 12.0" { + t.Errorf("expected application name to be 'Pro 12.0', got '%s'", f.CreatorName) } if f.VersionDate.Format("2006-01-02") != "2025-01-11" { t.Errorf("expected version date to be '2025-01-11', got '%s'", f.VersionDate.Format("2006-01-02")) } + print(f.Sectors[0]) }