Files
core/Block Records.go
2021-08-19 16:49:37 +02:00

215 lines
7.2 KiB
Go

/*
File Name: Block Encoding.go
Copyright: 2021 Peernet s.r.o.
Author: Peter Kleissner
Encoding of records inside blocks.
File records:
Offset Size Info
0 32 Hash blake3 of the file content
32 1 File type (low-level)
33 2 File format (high-level)
35 8 File size
43 2 Directory ID
45 2 Size of file name in bytes
47 2 Count of tags
49 ? File name
? ? Tags (TBD)
Directory records:
Offset Size Info
0 2 Directory ID
2 ? Directory name
*/
package core
import (
"encoding/binary"
"errors"
"github.com/PeernetOfficial/core/sanitize"
)
// BlockDecoded contains the decoded records from a block
type BlockDecoded struct {
Block
Files []BlockRecordFile // Files
User BlockRecordUser // User details
directories []BlockRecordDirectory // Internal list of directories for decoding files.
}
// RecordTypeX defines the type of the record
const (
RecordTypeUsername = 0 // Username. Arbitrary name defined by the user.
RecordTypeDirectory = 1 // Directory. Only valid in the context of the current block.
RecordTypeFile = 2 // File
RecordTypeContentRating = 3 // Content rating (positive).
RecordTypeContentReport = 4 // Content report (negative).
RecordTypeDelete = 5 // Delete previous record.
)
// block record structures
// BlockRecordUser specifies user information
type BlockRecordUser struct {
Valid bool // Whether the username is supplied
Name string // Arbitrary name of the user.
NameS string // Sanitized version of the name.
}
// BlockRecordDirectory is a directory, only valid within the same block.
type BlockRecordDirectory struct {
ID uint16 // ID, only valid within the same block
Name string // Name of the directory. Slashes (both backward and forward) mark subdirectories.
}
// BlockRecordFile is the metadata of a file published on the blockchain
type BlockRecordFile struct {
Hash []byte // Hash of the file data
Type uint8 // Type (low-level)
Format uint16 // Format (high-level)
Size uint64 // Size of the file
Directory string // Directory
Name string // File name
directoryID uint16 // Internal directory ID
// Tags todo
}
// Tag structure to be defined
// decodeBlockRecords decodes the raw records in the block and returns a high-level decoded structure
func decodeBlockRecords(block *Block) (decoded *BlockDecoded, err error) {
decoded = &BlockDecoded{Block: *block}
if decoded.Files, decoded.directories, err = decodeBlockRecordFiles(block.RecordsRaw); err != nil {
return nil, err
}
if user := decodeBlockRecordUser(block.RecordsRaw); user != nil {
decoded.User = *user
}
return decoded, nil
}
// decodeBlockRecordUser decodes the username, if available. Otherwise return nil.
func decodeBlockRecordUser(recordsRaw []BlockRecordRaw) (user *BlockRecordUser) {
for _, record := range recordsRaw {
if record.Type == RecordTypeUsername {
user = &BlockRecordUser{Valid: true, Name: string(record.Data), NameS: sanitize.Username(string(record.Data))}
// continue to seek for an overriding username record, even though that would be stupid within the same block
}
}
return
}
// encodeBlockRecordUser encodes the username
func encodeBlockRecordUser(user BlockRecordUser) (recordsRaw []BlockRecordRaw, err error) {
if user.Valid {
recordsRaw = append(recordsRaw, BlockRecordRaw{Type: RecordTypeUsername, Data: []byte(user.Name)})
}
return recordsRaw, nil
}
// decodeBlockRecordFiles decodes only file records. Other records are ignored
func decodeBlockRecordFiles(recordsRaw []BlockRecordRaw) (files []BlockRecordFile, directories []BlockRecordDirectory, err error) {
for _, record := range recordsRaw {
switch record.Type {
case RecordTypeDirectory:
if len(record.Data) < 3 {
return nil, nil, errors.New("decodeBlockRecordFiles directory record invalid size")
}
directory := &BlockRecordDirectory{}
directory.ID = binary.LittleEndian.Uint16(record.Data[0 : 0+2])
directory.Name = string(record.Data[2:])
directories = append(directories, *directory)
case RecordTypeFile:
if len(record.Data) < 49 {
return nil, nil, errors.New("decodeBlockRecordFiles file record invalid size")
}
file := BlockRecordFile{}
file.Hash = make([]byte, hashSize)
copy(file.Hash, record.Data[0:0+hashSize])
file.Type = record.Data[32]
file.Format = binary.LittleEndian.Uint16(record.Data[33 : 33+2])
file.Size = binary.LittleEndian.Uint64(record.Data[35 : 35+8])
directoryID := binary.LittleEndian.Uint16(record.Data[43 : 43+2])
filenameSize := binary.LittleEndian.Uint16(record.Data[45 : 45+2])
//countTags := binary.LittleEndian.Uint16(record.Data[47 : 47+2]) // future implementation of tags
if len(record.Data) < 49+int(filenameSize) {
return nil, nil, errors.New("decodeBlockRecordFiles file record invalid filename size")
}
file.Name = string(record.Data[49 : 49+filenameSize])
for n := range directories {
if directories[n].ID == directoryID {
file.Directory = directories[n].Name
break
}
}
files = append(files, file)
}
}
return files, directories, err
}
// encodeBlockRecordFiles encodes files into the block record data
// This function should be called grouped with all files in the same directory. The directory name is deduplicated; only unique directory records will be returned.
func encodeBlockRecordFiles(files []BlockRecordFile) (recordsRaw []BlockRecordRaw, err error) {
// First the directory records must be declared for any references by files
nextDirectoryID := uint16(1) // start as 1 to prevent collision with files without explicit directory
directoryList := make(map[string]int)
for n := range files {
files[n].Directory, files[n].Name = sanitize.Path(files[n].Directory, files[n].Name)
if files[n].Directory == "" {
continue
}
if directoryID, ok := directoryList[files[n].Directory]; ok {
files[n].directoryID = uint16(directoryID)
continue
}
// Create the new directory record
var directoryIDb [2]byte
binary.LittleEndian.PutUint16(directoryIDb[0:2], nextDirectoryID)
files[n].directoryID = nextDirectoryID
nextDirectoryID++
recordsRaw = append(recordsRaw, BlockRecordRaw{Type: RecordTypeDirectory, Data: append(directoryIDb[:], []byte(files[n].Directory)...)})
}
for n := range files {
var data [49]byte
if len(files[n].Hash) != hashSize {
return nil, errors.New("encodeBlockRecords invalid file hash")
}
copy(data[0:32], files[n].Hash[0:32])
data[32] = files[n].Type
binary.LittleEndian.PutUint16(data[33:33+2], files[n].Format)
binary.LittleEndian.PutUint64(data[35:35+8], files[n].Size)
binary.LittleEndian.PutUint16(data[43:43+2], files[n].directoryID)
filenameB := []byte(files[n].Name)
binary.LittleEndian.PutUint16(data[45:45+2], uint16(len(filenameB)))
binary.LittleEndian.PutUint16(data[47:47+2], uint16(0)) // Count of Tags (future use)
recordsRaw = append(recordsRaw, BlockRecordRaw{Type: RecordTypeFile, Data: append(data[:], filenameB...)})
}
return recordsRaw, nil
}