]> git.r.bdr.sh - rbdr/mobius/blobdiff - hotline/flattened_file_object.go
Update README.md
[rbdr/mobius] / hotline / flattened_file_object.go
index 8e2dbff750569e5640a83ad3f495db083abda5e1..99ce204569551931a296db469da803799cf75be3 100644 (file)
@@ -1,17 +1,20 @@
 package hotline
 
 import (
+       "bytes"
        "encoding/binary"
-       "fmt"
-       "os"
+       "io"
+       "slices"
 )
 
 type flattenedFileObject struct {
        FlatFileHeader                FlatFileHeader
-       FlatFileInformationForkHeader FlatFileInformationForkHeader
+       FlatFileInformationForkHeader FlatFileForkHeader
        FlatFileInformationFork       FlatFileInformationFork
-       FlatFileDataForkHeader        FlatFileDataForkHeader
-       FileData                      []byte
+       FlatFileDataForkHeader        FlatFileForkHeader
+       FlatFileResForkHeader         FlatFileForkHeader
+
+       readOffset int // Internal offset to track read progress
 }
 
 // FlatFileHeader is the first section of a "Flattened File Object".  All fields have static values.
@@ -19,216 +22,280 @@ type FlatFileHeader struct {
        Format    [4]byte  // Always "FILP"
        Version   [2]byte  // Always 1
        RSVD      [16]byte // Always empty zeros
-       ForkCount [2]byte  // Always 2
+       ForkCount [2]byte  // Number of forks, either 2 or 3 if there is a resource fork
+}
+
+type FlatFileInformationFork struct {
+       Platform         [4]byte // Operating System used. ("AMAC" or "MWIN")
+       TypeSignature    [4]byte // File type signature
+       CreatorSignature [4]byte // File creator signature
+       Flags            [4]byte
+       PlatformFlags    [4]byte
+       RSVD             [32]byte
+       CreateDate       [8]byte
+       ModifyDate       [8]byte
+       NameScript       [2]byte
+       NameSize         [2]byte // Length of file name (Maximum 128 characters)
+       Name             []byte  // File name
+       CommentSize      [2]byte // Length of the comment
+       Comment          []byte  // File comment
+
+       readOffset int // Internal offset to track read progress
 }
 
-// NewFlatFileHeader returns a FlatFileHeader struct
-func NewFlatFileHeader() FlatFileHeader {
-       return FlatFileHeader{
-               Format:    [4]byte{0x46, 0x49, 0x4c, 0x50}, // FILP
-               Version:   [2]byte{0, 1},
-               RSVD:      [16]byte{},
-               ForkCount: [2]byte{0, 2},
+func NewFlatFileInformationFork(fileName string, modifyTime [8]byte, typeSignature string, creatorSignature string) FlatFileInformationFork {
+       return FlatFileInformationFork{
+               Platform:         [4]byte{0x41, 0x4D, 0x41, 0x43},   // "AMAC" TODO: Remove hardcode to support "AWIN" Platform (maybe?)
+               TypeSignature:    [4]byte([]byte(typeSignature)),    // TODO: Don't infer types from filename
+               CreatorSignature: [4]byte([]byte(creatorSignature)), // TODO: Don't infer types from filename
+               PlatformFlags:    [4]byte{0, 0, 1, 0},               // TODO: What is this?
+               CreateDate:       modifyTime,                        // some filesystems don't support createTime
+               ModifyDate:       modifyTime,
+               Name:             []byte(fileName),
+               Comment:          []byte{}, // TODO: implement (maybe?)
        }
 }
 
-// FlatFileInformationForkHeader is the second section of a "Flattened File Object"
-type FlatFileInformationForkHeader struct {
-       ForkType        []byte // Always "INFO"
-       CompressionType []byte // Always 0; Compression was never implemented in the Hotline protocol
-       RSVD            []byte // Always zeros
-       DataSize        []byte // Size of the flat file information fork
+func (ffif *FlatFileInformationFork) FriendlyType() []byte {
+       if name, ok := friendlyCreatorNames[string(ffif.TypeSignature[:])]; ok {
+               return []byte(name)
+       }
+       return ffif.TypeSignature[:]
 }
 
-type FlatFileInformationFork struct {
-       Platform         []byte // Operating System used. ("AMAC" or "MWIN")
-       TypeSignature    []byte // File type signature
-       CreatorSignature []byte // File creator signature
-       Flags            []byte
-       PlatformFlags    []byte
-       RSVD             []byte
-       CreateDate       []byte
-       ModifyDate       []byte
-       NameScript       []byte // TODO: what is this?
-       NameSize         []byte // Length of file name (Maximum 128 characters)
-       Name             []byte // File name
-       CommentSize      []byte // Length of file comment
-       Comment          []byte // File comment
-}
-
-func NewFlatFileInformationFork(fileName string) FlatFileInformationFork {
-       return FlatFileInformationFork{
-               Platform:         []byte("AMAC"),                                         // TODO: Remove hardcode to support "AWIN" Platform (maybe?)
-               TypeSignature:    []byte(fileTypeFromFilename(fileName)),                 // TODO: Don't infer types from filename
-               CreatorSignature: []byte(fileCreatorFromFilename(fileName)),              // TODO: Don't infer types from filename
-               Flags:            []byte{0, 0, 0, 0},                                     // TODO: What is this?
-               PlatformFlags:    []byte{0, 0, 1, 0},                                     // TODO: What is this?
-               RSVD:             make([]byte, 32),                                       // Unimplemented in Hotline Protocol
-               CreateDate:       []byte{0x07, 0x70, 0x00, 0x00, 0xba, 0x74, 0x24, 0x73}, // TODO: implement
-               ModifyDate:       []byte{0x07, 0x70, 0x00, 0x00, 0xba, 0x74, 0x24, 0x73}, // TODO: implement
-               NameScript:       make([]byte, 2),                                        // TODO: What is this?
-               Name:             []byte(fileName),
-               Comment:          []byte("TODO"), // TODO: implement (maybe?)
+func (ffif *FlatFileInformationFork) FriendlyCreator() []byte {
+       if name, ok := friendlyCreatorNames[string(ffif.CreatorSignature[:])]; ok {
+               return []byte(name)
        }
+       return ffif.CreatorSignature[:]
 }
 
-// Size of the flat file information fork, which is the fixed size of 72 bytes
-// plus the number of bytes in the FileName
-// TODO: plus the size of the Comment!
-func (ffif FlatFileInformationFork) DataSize() []byte {
+func (ffif *FlatFileInformationFork) SetComment(comment []byte) error {
+       commentSize := make([]byte, 2)
+       ffif.Comment = comment
+       binary.BigEndian.PutUint16(commentSize, uint16(len(comment)))
+       ffif.CommentSize = [2]byte(commentSize)
+       // TODO: return err if comment is too long
+       return nil
+}
+
+// DataSize calculates the size of the flat file information fork, which is
+// 72 bytes for the fixed length fields plus the length of the Name + Comment
+func (ffif *FlatFileInformationFork) DataSize() []byte {
        size := make([]byte, 4)
-       nameLen := len(ffif.Name)
-       //TODO: Can I do math directly on two byte slices?
-       dataSize := nameLen + 74
+
+       dataSize := len(ffif.Name) + len(ffif.Comment) + 74 // 74 = len of fixed size headers
 
        binary.BigEndian.PutUint32(size, uint32(dataSize))
 
        return size
 }
 
-func (ffo flattenedFileObject) TransferSize() []byte {
-       payloadSize := len(ffo.Payload())
-       dataSize := binary.BigEndian.Uint32(ffo.FlatFileDataForkHeader.DataSize)
+func (ffif *FlatFileInformationFork) Size() [4]byte {
+       size := [4]byte{}
+
+       dataSize := len(ffif.Name) + len(ffif.Comment) + 74 // 74 = len of fixed size headers
+
+       binary.BigEndian.PutUint32(size[:], uint32(dataSize))
+
+       return size
+}
+
+func (ffo *flattenedFileObject) TransferSize(offset int64) []byte {
+       ffoCopy := *ffo
+
+       // get length of the flattenedFileObject, including the info fork
+       b, _ := io.ReadAll(&ffoCopy)
+       payloadSize := len(b)
+
+       // length of data fork
+       dataSize := binary.BigEndian.Uint32(ffo.FlatFileDataForkHeader.DataSize[:])
 
-       transferSize := make([]byte, 4)
-       binary.BigEndian.PutUint32(transferSize, dataSize+uint32(payloadSize))
+       // length of resource fork
+       resForkSize := binary.BigEndian.Uint32(ffo.FlatFileResForkHeader.DataSize[:])
 
-       return transferSize
+       size := make([]byte, 4)
+       binary.BigEndian.PutUint32(size, dataSize+resForkSize+uint32(payloadSize)-uint32(offset))
+
+       return size
 }
 
-func (ffif FlatFileInformationFork) ReadNameSize() []byte {
+func (ffif *FlatFileInformationFork) ReadNameSize() []byte {
        size := make([]byte, 2)
        binary.BigEndian.PutUint16(size, uint16(len(ffif.Name)))
 
        return size
 }
 
-type FlatFileDataForkHeader struct {
-       ForkType        []byte
-       CompressionType []byte
-       RSVD            []byte
-       DataSize        []byte
+type FlatFileForkHeader struct {
+       ForkType        [4]byte // Either INFO, DATA or MACR
+       CompressionType [4]byte
+       RSVD            [4]byte
+       DataSize        [4]byte
 }
 
-func NewFlatFileDataForkHeader() FlatFileDataForkHeader {
-       return FlatFileDataForkHeader{
-               ForkType:        []byte("DATA"),
-               CompressionType: []byte{0, 0, 0, 0},
-               RSVD:            []byte{0, 0, 0, 0},
-               //      DataSize:        []byte{0, 0, 0x03, 0xc3},
+func (ffif *FlatFileInformationFork) Read(p []byte) (int, error) {
+       buf := slices.Concat(
+               ffif.Platform[:],
+               ffif.TypeSignature[:],
+               ffif.CreatorSignature[:],
+               ffif.Flags[:],
+               ffif.PlatformFlags[:],
+               ffif.RSVD[:],
+               ffif.CreateDate[:],
+               ffif.ModifyDate[:],
+               ffif.NameScript[:],
+               ffif.ReadNameSize(),
+               ffif.Name,
+               ffif.CommentSize[:],
+               ffif.Comment,
+       )
+
+       if ffif.readOffset >= len(buf) {
+               return 0, io.EOF // All bytes have been read
        }
+
+       n := copy(p, buf[ffif.readOffset:])
+       ffif.readOffset += n
+
+       return n, nil
 }
 
-// ReadFlattenedFileObject parses a byte slice into a flattenedFileObject
-func ReadFlattenedFileObject(bytes []byte) flattenedFileObject {
-       nameSize := bytes[110:112]
+// Write implements the io.Writer interface for FlatFileInformationFork
+func (ffif *FlatFileInformationFork) Write(p []byte) (int, error) {
+       nameSize := p[70:72]
        bs := binary.BigEndian.Uint16(nameSize)
+       total := 72 + bs
+
+       ffif.Platform = [4]byte(p[0:4])
+       ffif.TypeSignature = [4]byte(p[4:8])
+       ffif.CreatorSignature = [4]byte(p[8:12])
+       ffif.Flags = [4]byte(p[12:16])
+       ffif.PlatformFlags = [4]byte(p[16:20])
+       ffif.RSVD = [32]byte(p[20:52])
+       ffif.CreateDate = [8]byte(p[52:60])
+       ffif.ModifyDate = [8]byte(p[60:68])
+       ffif.NameScript = [2]byte(p[68:70])
+       ffif.NameSize = [2]byte(p[70:72])
+       ffif.Name = p[72:total]
+
+       if len(p) > int(total) {
+               ffif.CommentSize = [2]byte(p[total : total+2])
+               commentLen := binary.BigEndian.Uint16(ffif.CommentSize[:])
+               commentStartPos := int(total) + 2
+               commentEndPos := int(total) + 2 + int(commentLen)
+
+               ffif.Comment = p[commentStartPos:commentEndPos]
+
+               //total = uint16(commentEndPos)
+       }
 
-       nameEnd := 112 + bs
-
-       commentSize := bytes[nameEnd : nameEnd+2]
-       commentLen := binary.BigEndian.Uint16(commentSize)
-
-       commentStartPos := int(nameEnd) + 2
-       commentEndPos := int(nameEnd) + 2 + int(commentLen)
-
-       comment := bytes[commentStartPos:commentEndPos]
-
-       //dataSizeField := bytes[nameEnd+14+commentLen : nameEnd+18+commentLen]
-       //dataSize := binary.BigEndian.Uint32(dataSizeField)
-
-       ffo := flattenedFileObject{
-               FlatFileHeader: NewFlatFileHeader(),
-               FlatFileInformationForkHeader: FlatFileInformationForkHeader{
-                       ForkType:        bytes[24:28],
-                       CompressionType: bytes[28:32],
-                       RSVD:            bytes[32:36],
-                       DataSize:        bytes[36:40],
-               },
-               FlatFileInformationFork: FlatFileInformationFork{
-                       Platform:         bytes[40:44],
-                       TypeSignature:    bytes[44:48],
-                       CreatorSignature: bytes[48:52],
-                       Flags:            bytes[52:56],
-                       PlatformFlags:    bytes[56:60],
-                       RSVD:             bytes[60:92],
-                       CreateDate:       bytes[92:100],
-                       ModifyDate:       bytes[100:108],
-                       NameScript:       bytes[108:110],
-                       NameSize:         bytes[110:112],
-                       Name:             bytes[112:nameEnd],
-                       CommentSize:      bytes[nameEnd : nameEnd+2],
-                       Comment:          comment,
-               },
-               FlatFileDataForkHeader: FlatFileDataForkHeader{
-                       ForkType:        bytes[commentEndPos : commentEndPos+4],
-                       CompressionType: bytes[commentEndPos+4 : commentEndPos+8],
-                       RSVD:            bytes[commentEndPos+8 : commentEndPos+12],
-                       DataSize:        bytes[commentEndPos+12 : commentEndPos+16],
-               },
+       return len(p), nil
+}
+
+func (ffif *FlatFileInformationFork) UnmarshalBinary(b []byte) error {
+       nameSize := b[70:72]
+       bs := binary.BigEndian.Uint16(nameSize)
+       nameEnd := 72 + bs
+
+       ffif.Platform = [4]byte(b[0:4])
+       ffif.TypeSignature = [4]byte(b[4:8])
+       ffif.CreatorSignature = [4]byte(b[8:12])
+       ffif.Flags = [4]byte(b[12:16])
+       ffif.PlatformFlags = [4]byte(b[16:20])
+       ffif.RSVD = [32]byte(b[20:52])
+       ffif.CreateDate = [8]byte(b[52:60])
+       ffif.ModifyDate = [8]byte(b[60:68])
+       ffif.NameScript = [2]byte(b[68:70])
+       ffif.NameSize = [2]byte(b[70:72])
+       ffif.Name = b[72:nameEnd]
+
+       if len(b) > int(nameEnd) {
+               ffif.CommentSize = [2]byte(b[nameEnd : nameEnd+2])
+               commentLen := binary.BigEndian.Uint16(ffif.CommentSize[:])
+
+               commentStartPos := int(nameEnd) + 2
+               commentEndPos := int(nameEnd) + 2 + int(commentLen)
+
+               ffif.Comment = b[commentStartPos:commentEndPos]
        }
 
-       return ffo
+       return nil
 }
 
-func (f flattenedFileObject) Payload() []byte {
-       var out []byte
-       out = append(out, f.FlatFileHeader.Format[:]...)
-       out = append(out, f.FlatFileHeader.Version[:]...)
-       out = append(out, f.FlatFileHeader.RSVD[:]...)
-       out = append(out, f.FlatFileHeader.ForkCount[:]...)
+// Read implements the io.Reader interface for flattenedFileObject
+func (ffo *flattenedFileObject) Read(p []byte) (int, error) {
+       buf := slices.Concat(
+               ffo.FlatFileHeader.Format[:],
+               ffo.FlatFileHeader.Version[:],
+               ffo.FlatFileHeader.RSVD[:],
+               ffo.FlatFileHeader.ForkCount[:],
+               []byte("INFO"),
+               []byte{0, 0, 0, 0},
+               make([]byte, 4),
+               ffo.FlatFileInformationFork.DataSize(),
+               ffo.FlatFileInformationFork.Platform[:],
+               ffo.FlatFileInformationFork.TypeSignature[:],
+               ffo.FlatFileInformationFork.CreatorSignature[:],
+               ffo.FlatFileInformationFork.Flags[:],
+               ffo.FlatFileInformationFork.PlatformFlags[:],
+               ffo.FlatFileInformationFork.RSVD[:],
+               ffo.FlatFileInformationFork.CreateDate[:],
+               ffo.FlatFileInformationFork.ModifyDate[:],
+               ffo.FlatFileInformationFork.NameScript[:],
+               ffo.FlatFileInformationFork.ReadNameSize(),
+               ffo.FlatFileInformationFork.Name,
+               ffo.FlatFileInformationFork.CommentSize[:],
+               ffo.FlatFileInformationFork.Comment,
+               ffo.FlatFileDataForkHeader.ForkType[:],
+               ffo.FlatFileDataForkHeader.CompressionType[:],
+               ffo.FlatFileDataForkHeader.RSVD[:],
+               ffo.FlatFileDataForkHeader.DataSize[:],
+       )
+
+       if ffo.readOffset >= len(buf) {
+               return 0, io.EOF // All bytes have been read
+       }
 
-       out = append(out, []byte("INFO")...)
-       out = append(out, []byte{0, 0, 0, 0}...)
-       out = append(out, make([]byte, 4)...)
-       out = append(out, f.FlatFileInformationFork.DataSize()...)
+       n := copy(p, buf[ffo.readOffset:])
+       ffo.readOffset += n
 
-       out = append(out, f.FlatFileInformationFork.Platform...)
-       out = append(out, f.FlatFileInformationFork.TypeSignature...)
-       out = append(out, f.FlatFileInformationFork.CreatorSignature...)
-       out = append(out, f.FlatFileInformationFork.Flags...)
-       out = append(out, f.FlatFileInformationFork.PlatformFlags...)
-       out = append(out, f.FlatFileInformationFork.RSVD...)
-       out = append(out, f.FlatFileInformationFork.CreateDate...)
-       out = append(out, f.FlatFileInformationFork.ModifyDate...)
-       out = append(out, f.FlatFileInformationFork.NameScript...)
-       out = append(out, f.FlatFileInformationFork.ReadNameSize()...)
-       out = append(out, f.FlatFileInformationFork.Name...)
+       return n, nil
+}
 
-       // TODO: Implement commentlen and comment field
-       out = append(out, []byte{0, 0}...)
+func (ffo *flattenedFileObject) ReadFrom(r io.Reader) (int64, error) {
+       var n int64
 
-       out = append(out, f.FlatFileDataForkHeader.ForkType...)
-       out = append(out, f.FlatFileDataForkHeader.CompressionType...)
-       out = append(out, f.FlatFileDataForkHeader.RSVD...)
-       out = append(out, f.FlatFileDataForkHeader.DataSize...)
+       if err := binary.Read(r, binary.BigEndian, &ffo.FlatFileHeader); err != nil {
+               return n, err
+       }
 
-       return out
-}
+       if err := binary.Read(r, binary.BigEndian, &ffo.FlatFileInformationForkHeader); err != nil {
+               return n, err
+       }
 
-func NewFlattenedFileObject(filePath, fileName string) (*flattenedFileObject, error) {
-       file, err := os.Open(fmt.Sprintf("%v/%v", filePath, fileName))
-       if err != nil {
-               return nil, err
+       dataLen := binary.BigEndian.Uint32(ffo.FlatFileInformationForkHeader.DataSize[:])
+       ffifBuf := make([]byte, dataLen)
+       if _, err := io.ReadFull(r, ffifBuf); err != nil {
+               return n, err
        }
-       defer file.Close()
 
-       fileInfo, err := file.Stat()
+       _, err := io.Copy(&ffo.FlatFileInformationFork, bytes.NewReader(ffifBuf))
        if err != nil {
-               return nil, err
+               return n, err
+       }
+
+       if err := binary.Read(r, binary.BigEndian, &ffo.FlatFileDataForkHeader); err != nil {
+               return n, err
        }
 
-       dataSize := make([]byte, 4)
-       binary.BigEndian.PutUint32(dataSize, uint32(fileInfo.Size()))
-
-       return &flattenedFileObject{
-               FlatFileHeader:          NewFlatFileHeader(),
-               FlatFileInformationFork: NewFlatFileInformationFork(fileName),
-               FlatFileDataForkHeader: FlatFileDataForkHeader{
-                       ForkType:        []byte("DATA"),
-                       CompressionType: []byte{0, 0, 0, 0},
-                       RSVD:            []byte{0, 0, 0, 0},
-                       DataSize:        dataSize,
-               },
-       }, nil
+       return n, nil
+}
+
+func (ffo *flattenedFileObject) dataSize() int64 {
+       return int64(binary.BigEndian.Uint32(ffo.FlatFileDataForkHeader.DataSize[:]))
+}
+
+func (ffo *flattenedFileObject) rsrcSize() int64 {
+       return int64(binary.BigEndian.Uint32(ffo.FlatFileResForkHeader.DataSize[:]))
 }