package blobtree import ( "crypto/sha256" "encoding/binary" "errors" "fmt" "os" "path/filepath" "strconv" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" ) const ( defaultVersion = 0 sizeOfVersion = 1 sizeOfCount = 8 sizeOfDataLength = 8 sizeOfContainerID = sha256.Size sizeOfObjectID = sha256.Size dataExtension = ".data" ) var ( errFileToSmall = errors.New("invalid file content: not enough bytes to read count of records") errInvalidFileContentVersion = errors.New("invalid file content: not enough bytes to read record version") errInvalidFileContentContainerID = errors.New("invalid file content: not enough bytes to read container ID") errInvalidFileContentObjectID = errors.New("invalid file content: not enough bytes to read object ID") errInvalidFileContentLength = errors.New("invalid file content: not enough bytes to read data length") errInvalidFileContentData = errors.New("invalid file content: not enough bytes to read data") ) type objectData struct { Version byte Address oid.Address Data []byte } func (b *BlobTree) readFileContent(path string) ([]objectData, error) { rawData, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return []objectData{}, nil } return nil, err } return b.unmarshalSlice(rawData) } func (b *BlobTree) unmarshalSlice(data []byte) ([]objectData, error) { if len(data) < sizeOfCount { return nil, errFileToSmall } count := binary.LittleEndian.Uint64(data[:8]) result := make([]objectData, 0, count) data = data[sizeOfCount:] var idx uint64 for idx = 0; idx < count; idx++ { record, read, err := b.unmarshalRecord(data) if err != nil { return nil, err } result = append(result, record) data = data[read:] } return result, nil } func (b *BlobTree) unmarshalRecord(data []byte) (objectData, uint64, error) { if len(data) < sizeOfVersion { return objectData{}, 0, errInvalidFileContentVersion } var result objectData var read uint64 result.Version = data[0] if result.Version != defaultVersion { return objectData{}, 0, fmt.Errorf("invalid file content: unknown version %d", result.Version) } read += sizeOfVersion if len(data[read:]) < sizeOfContainerID { return objectData{}, 0, errInvalidFileContentContainerID } var contID cid.ID if err := contID.Decode(data[read : read+sizeOfContainerID]); err != nil { return objectData{}, 0, fmt.Errorf("invalid file content: failed to read container ID: %w", err) } read += sizeOfContainerID if len(data[read:]) < sizeOfObjectID { return objectData{}, 0, errInvalidFileContentObjectID } var objID oid.ID if err := objID.Decode(data[read : read+sizeOfObjectID]); err != nil { return objectData{}, 0, fmt.Errorf("invalid file content: failed to read object ID: %w", err) } read += sizeOfObjectID result.Address.SetContainer(contID) result.Address.SetObject(objID) if len(data[read:]) < sizeOfDataLength { return objectData{}, 0, errInvalidFileContentLength } dataLength := binary.LittleEndian.Uint64(data[read : read+sizeOfDataLength]) read += sizeOfDataLength if uint64(len(data[read:])) < dataLength { return objectData{}, 0, errInvalidFileContentData } result.Data = make([]byte, dataLength) copy(result.Data, data[read:read+dataLength]) read += dataLength return result, read, nil } func (b *BlobTree) saveContentToFile(records []objectData, path string) (uint64, error) { data, err := b.marshalSlice(records) if err != nil { return 0, err } return uint64(len(data)), b.writeFile(path, data) } func (b *BlobTree) writeFile(p string, data []byte) error { f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_EXCL|os.O_SYNC, b.cfg.permissions) if err != nil { return err } _, err = f.Write(data) if err1 := f.Close(); err1 != nil && err == nil { err = err1 } return err } func (b *BlobTree) marshalSlice(records []objectData) ([]byte, error) { buf := make([]byte, b.estimateSize(records)) result := buf binary.LittleEndian.PutUint64(buf, uint64(len(records))) buf = buf[sizeOfCount:] for _, record := range records { written := b.marshalRecord(record, buf) buf = buf[written:] } return result, nil } func (b *BlobTree) marshalRecord(record objectData, dst []byte) uint64 { var written uint64 dst[0] = record.Version dst = dst[sizeOfVersion:] written += sizeOfVersion record.Address.Container().Encode(dst) dst = dst[sizeOfContainerID:] written += sizeOfContainerID record.Address.Object().Encode(dst) dst = dst[sizeOfObjectID:] written += sizeOfObjectID binary.LittleEndian.PutUint64(dst, uint64(len(record.Data))) dst = dst[sizeOfDataLength:] written += sizeOfDataLength copy(dst, record.Data) written += uint64(len(record.Data)) return written } func (b *BlobTree) estimateSize(records []objectData) uint64 { var result uint64 result += sizeOfCount for _, record := range records { result += (sizeOfVersion + sizeOfContainerID + sizeOfObjectID + sizeOfDataLength) result += uint64(len(record.Data)) } return result } func (b *BlobTree) getFilePath(dir string, idx uint64) string { return filepath.Join(dir, strconv.FormatUint(idx, 16)+dataExtension) }