X-Git-Url: https://git.r.bdr.sh/rbdr/mobius/blobdiff_plain/2728d12b6169a978aa3dc2e35390923d2eecd295..a2ef262a164fc735b9b8471ac0c8001eea2b9bf6:/hotline/flattened_file_object.go diff --git a/hotline/flattened_file_object.go b/hotline/flattened_file_object.go index bd281e0..fbc319b 100644 --- a/hotline/flattened_file_object.go +++ b/hotline/flattened_file_object.go @@ -1,16 +1,20 @@ package hotline import ( + "bytes" "encoding/binary" - "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. @@ -18,210 +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).TypeCode), // TODO: Don't infer types from filename - CreatorSignature: []byte(fileTypeFromFilename(fileName).CreatorCode), // 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), - CommentSize: []byte{0, 4}, - 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[:] +} + +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 { +func (ffif *FlatFileInformationFork) DataSize() []byte { size := make([]byte, 4) - //TODO: Can I do math directly on two byte slices? - dataSize := len(ffif.Name) + len(ffif.Comment) + 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.BinaryMarshal()) - 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 } -// ReadFlattenedFileObject parses a byte slice into a flattenedFileObject -func ReadFlattenedFileObject(bytes []byte) flattenedFileObject { - nameSize := bytes[110:112] +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 +} + +// 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 -} - -func (f flattenedFileObject) BinaryMarshal() []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[:]...) - - 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()...) - - 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...) - out = append(out, f.FlatFileInformationFork.CommentSize...) - out = append(out, f.FlatFileInformationFork.Comment...) - - out = append(out, f.FlatFileDataForkHeader.ForkType...) - out = append(out, f.FlatFileDataForkHeader.CompressionType...) - out = append(out, f.FlatFileDataForkHeader.RSVD...) - out = append(out, f.FlatFileDataForkHeader.DataSize...) - - return out -} - -func NewFlattenedFileObject(fileRoot string, filePath, fileName []byte) (*flattenedFileObject, error) { - fullFilePath, err := readPath(fileRoot, filePath, fileName) - if err != nil { - return nil, err + return nil +} + +// 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 } - file, err := os.Open(fullFilePath) - if err != nil { - return nil, err + + n := copy(p, buf[ffo.readOffset:]) + ffo.readOffset += n + + return n, nil +} + +func (ffo *flattenedFileObject) ReadFrom(r io.Reader) (int64, error) { + var n int64 + + if err := binary.Read(r, binary.BigEndian, &ffo.FlatFileHeader); err != nil { + return n, err + } + + if err := binary.Read(r, binary.BigEndian, &ffo.FlatFileInformationForkHeader); err != nil { + return n, 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 } - dataSize := make([]byte, 4) - binary.BigEndian.PutUint32(dataSize, uint32(fileInfo.Size())) - - return &flattenedFileObject{ - FlatFileHeader: NewFlatFileHeader(), - FlatFileInformationFork: NewFlatFileInformationFork(string(fileName)), - FlatFileDataForkHeader: FlatFileDataForkHeader{ - ForkType: []byte("DATA"), - CompressionType: []byte{0, 0, 0, 0}, - RSVD: []byte{0, 0, 0, 0}, - DataSize: dataSize, - }, - }, nil + if err := binary.Read(r, binary.BigEndian, &ffo.FlatFileDataForkHeader); err != nil { + return n, err + } + + 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[:])) }