mirror of
https://github.com/Quad4-Software/Reticulum-Go
synced 2026-08-29 23:48:44 -04:00
977 lines
23 KiB
Go
977 lines
23 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
|
|
package resource
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"quad4/msgpack/v5/pkg/msgpack"
|
|
)
|
|
|
|
type Resource struct {
|
|
mutex sync.RWMutex
|
|
data []byte
|
|
sourceData []byte
|
|
fileHandle io.ReadWriteSeeker
|
|
fileName string
|
|
hash []byte
|
|
randomHash []byte
|
|
originalHash []byte
|
|
status byte
|
|
compressed bool
|
|
autoCompress bool
|
|
encrypted bool
|
|
split bool
|
|
segments uint16
|
|
segmentIndex uint16
|
|
totalSegments uint16
|
|
completedParts map[uint16]bool
|
|
transferSize int64
|
|
dataSize int64
|
|
progress float64
|
|
createdAt time.Time
|
|
completedAt time.Time
|
|
callback func(*Resource)
|
|
progressCallback func(*Resource)
|
|
readOffset int64
|
|
requestID []byte
|
|
isResponse bool
|
|
hashmap []byte
|
|
outboundCipher []byte
|
|
outboundPartSent []bool
|
|
outboundSentCount int
|
|
metadata map[string]any
|
|
metadataPacked []byte
|
|
}
|
|
|
|
func New(data any, autoCompress bool) (*Resource, error) {
|
|
r := &Resource{
|
|
status: StatusPending,
|
|
compressed: false,
|
|
autoCompress: autoCompress,
|
|
completedParts: make(map[uint16]bool),
|
|
createdAt: time.Now(),
|
|
progress: 0.0,
|
|
}
|
|
|
|
switch v := data.(type) {
|
|
case []byte:
|
|
r.data = v
|
|
r.dataSize = int64(len(v))
|
|
case io.ReadWriteSeeker:
|
|
r.fileHandle = v
|
|
size, err := v.Seek(0, io.SeekEnd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.dataSize = size
|
|
_, err = v.Seek(0, io.SeekStart)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if namer, ok := v.(interface{ Name() string }); ok {
|
|
r.fileName = namer.Name()
|
|
}
|
|
default:
|
|
return nil, errors.New("unsupported data type")
|
|
}
|
|
|
|
// Calculate segments needed
|
|
r.segments = uint16((r.dataSize + DefaultSegmentSize - 1) / DefaultSegmentSize) // #nosec G115
|
|
if r.segments > MaxSegments {
|
|
return nil, errors.New("resource too large")
|
|
}
|
|
|
|
// Calculate transfer size
|
|
r.transferSize = r.dataSize
|
|
if r.autoCompress {
|
|
// Estimate compressed size based on data type and content
|
|
if r.data != nil {
|
|
// For in-memory data, we can analyze content
|
|
compressibility := estimateCompressibility(r.data)
|
|
r.transferSize = int64(float64(r.dataSize) * compressibility)
|
|
} else if r.fileHandle != nil {
|
|
// For file handles, use extension-based estimation
|
|
ext := strings.ToLower(filepath.Ext(r.fileName))
|
|
r.transferSize = estimateFileCompression(r.dataSize, ext)
|
|
}
|
|
|
|
// Ensure minimum size and add compression overhead
|
|
if r.transferSize < r.dataSize/10 {
|
|
r.transferSize = r.dataSize / 10
|
|
}
|
|
r.transferSize += 64 // Header overhead for compression
|
|
}
|
|
|
|
// Calculate resource hash
|
|
if err := r.calculateHash(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func (r *Resource) calculateHash() error {
|
|
h := sha256.New()
|
|
|
|
if r.data != nil {
|
|
h.Write(r.data)
|
|
} else if r.fileHandle != nil {
|
|
if _, err := r.fileHandle.Seek(0, io.SeekStart); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(h, r.fileHandle); err != nil {
|
|
return err
|
|
}
|
|
if _, err := r.fileHandle.Seek(0, io.SeekStart); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
r.hash = h.Sum(nil)
|
|
return nil
|
|
}
|
|
|
|
func (r *Resource) GetHash() []byte {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return append([]byte{}, r.hash...)
|
|
}
|
|
|
|
func (r *Resource) GetStatus() byte {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return r.status
|
|
}
|
|
|
|
func (r *Resource) GetProgress() float64 {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return r.progress
|
|
}
|
|
|
|
func (r *Resource) GetTransferSize() int64 {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return r.transferSize
|
|
}
|
|
|
|
func (r *Resource) GetDataSize() int64 {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return r.dataSize
|
|
}
|
|
|
|
func (r *Resource) GetSegments() uint16 {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return r.segments
|
|
}
|
|
|
|
func (r *Resource) Cancel() {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
|
|
if r.status == StatusPending || r.status == StatusActive {
|
|
r.status = StatusCancelled
|
|
r.completedAt = time.Now()
|
|
if r.callback != nil {
|
|
r.callback(r)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Resource) SetCallback(callback func(*Resource)) {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
r.callback = callback
|
|
}
|
|
|
|
func (r *Resource) SetProgressCallback(callback func(*Resource)) {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
r.progressCallback = callback
|
|
}
|
|
|
|
// GetSegmentData returns the data for a specific segment
|
|
func (r *Resource) GetSegmentData(segment uint16) ([]byte, error) {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
|
|
if segment >= r.segments {
|
|
return nil, errors.New("invalid segment number")
|
|
}
|
|
|
|
start := int64(segment) * DefaultSegmentSize
|
|
size := DefaultSegmentSize
|
|
if segment == r.segments-1 {
|
|
size = int(r.dataSize - start)
|
|
}
|
|
|
|
data := make([]byte, size)
|
|
if r.data != nil {
|
|
copy(data, r.data[start:start+int64(size)])
|
|
return data, nil
|
|
}
|
|
|
|
if r.fileHandle != nil {
|
|
if _, err := r.fileHandle.Seek(start, io.SeekStart); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := io.ReadFull(r.fileHandle, data); err != nil {
|
|
return nil, err
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
return nil, errors.New("no data source available")
|
|
}
|
|
|
|
// MarkSegmentComplete marks a segment as completed and updates progress
|
|
func (r *Resource) MarkSegmentComplete(segment uint16) {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
|
|
if segment >= r.segments {
|
|
return
|
|
}
|
|
|
|
r.completedParts[segment] = true
|
|
completed := len(r.completedParts)
|
|
r.progress = float64(completed) / float64(r.segments)
|
|
|
|
if r.progressCallback != nil {
|
|
r.progressCallback(r)
|
|
}
|
|
|
|
// Check if all segments are complete
|
|
if completed == int(r.segments) {
|
|
r.status = StatusComplete
|
|
r.completedAt = time.Now()
|
|
if r.callback != nil {
|
|
r.callback(r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// IsSegmentComplete checks if a specific segment is complete
|
|
func (r *Resource) IsSegmentComplete(segment uint16) bool {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return r.completedParts[segment]
|
|
}
|
|
|
|
// Activate marks the resource as active
|
|
func (r *Resource) Activate() {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
if r.status == StatusPending {
|
|
r.status = StatusActive
|
|
}
|
|
}
|
|
|
|
// SetFailed marks the resource as failed
|
|
func (r *Resource) SetFailed() {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
if r.status != StatusComplete {
|
|
r.status = StatusFailed
|
|
r.completedAt = time.Now()
|
|
if r.callback != nil {
|
|
r.callback(r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper functions for compression estimation
|
|
func estimateCompressibility(data []byte) float64 {
|
|
// Sample the data to estimate compressibility
|
|
sampleSize := min(len(data), 4096)
|
|
|
|
// Count unique bytes in sample
|
|
var seen [256]bool
|
|
unique := 0
|
|
for i := range sampleSize {
|
|
b := data[i]
|
|
if !seen[b] {
|
|
seen[b] = true
|
|
unique++
|
|
}
|
|
}
|
|
|
|
// Calculate entropy-based compression estimate
|
|
uniqueRatio := float64(unique) / float64(sampleSize)
|
|
return CompressionEntropyBase + (CompressionEntropyRange * uniqueRatio)
|
|
}
|
|
|
|
func estimateFileCompression(size int64, extension string) int64 {
|
|
var ratio float64
|
|
switch extension {
|
|
case ".txt", ".log", ".json", ".xml", ".html":
|
|
ratio = CompressionRatioText
|
|
case ".csv":
|
|
ratio = CompressionRatioCSV
|
|
case ".doc":
|
|
ratio = CompressionRatioOfficeLegacy
|
|
case ".docx", ".pdf":
|
|
ratio = CompressionRatioOfficeModern
|
|
case ".jpg", ".jpeg", ".png", ".gif", ".mp3", ".mp4", ".zip", ".gz", ".rar":
|
|
ratio = CompressionRatioAlreadyPacked
|
|
default:
|
|
ratio = CompressionRatioUnknown
|
|
}
|
|
|
|
return int64(float64(size) * ratio)
|
|
}
|
|
|
|
// PrepareOutboundForLink builds the inner ciphertext blob, hash, hashmap, and
|
|
// segment counts for sending a resource compatible with Reticulum peers.
|
|
// sdu is the maximum plaintext length per link data packet (link MDU).
|
|
// Resources larger than MaxEfficientSize are split across multiple advertisements
|
|
// matching Python Resource.MAX_EFFICIENT_SIZE behavior.
|
|
func (r *Resource) PrepareOutboundForLink(encrypt func([]byte) ([]byte, error), sdu int) error {
|
|
if sdu <= 0 {
|
|
return errors.New("invalid sdu")
|
|
}
|
|
if encrypt == nil {
|
|
return errors.New("nil encrypt")
|
|
}
|
|
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
|
|
r.outboundPartSent = nil
|
|
r.outboundSentCount = 0
|
|
|
|
if err := r.ensureMetadataPackedLocked(); err != nil {
|
|
return err
|
|
}
|
|
metaLen := len(r.metadataPacked)
|
|
|
|
fileSize, err := r.rawBodySizeLocked()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
totalSize := fileSize + int64(metaLen)
|
|
|
|
if r.segmentIndex == 0 {
|
|
r.segmentIndex = 1
|
|
}
|
|
|
|
if totalSize <= int64(MaxEfficientSize) {
|
|
r.split = false
|
|
r.totalSegments = 1
|
|
r.segmentIndex = 1
|
|
body, err := r.readRawBodyLocked(0, fileSize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return r.finishPrepareSegmentLocked(encrypt, sdu, body, true)
|
|
}
|
|
|
|
r.split = true
|
|
r.totalSegments = uint16((totalSize-1)/int64(MaxEfficientSize) + 1) // #nosec G115
|
|
if r.segmentIndex < 1 || r.segmentIndex > r.totalSegments {
|
|
return fmt.Errorf("invalid segment index %d of %d", r.segmentIndex, r.totalSegments)
|
|
}
|
|
|
|
firstRead := int64(MaxEfficientSize) - int64(metaLen)
|
|
if firstRead < 0 {
|
|
return errors.New("metadata exceeds efficient segment size")
|
|
}
|
|
|
|
var offset, length int64
|
|
includeMeta := false
|
|
if r.segmentIndex == 1 {
|
|
offset = 0
|
|
length = min(firstRead, fileSize)
|
|
includeMeta = true
|
|
} else {
|
|
offset = firstRead + int64(r.segmentIndex-2)*int64(MaxEfficientSize)
|
|
length = int64(MaxEfficientSize)
|
|
if offset+length > fileSize {
|
|
length = fileSize - offset
|
|
}
|
|
if length < 0 {
|
|
length = 0
|
|
}
|
|
}
|
|
|
|
body, err := r.readRawBodyLocked(offset, length)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return r.finishPrepareSegmentLocked(encrypt, sdu, body, includeMeta)
|
|
}
|
|
|
|
// PrepareNextOutboundSegment advances to the next split segment and prepares it.
|
|
func (r *Resource) PrepareNextOutboundSegment(encrypt func([]byte) ([]byte, error), sdu int) error {
|
|
r.mutex.Lock()
|
|
if !r.split || r.segmentIndex >= r.totalSegments {
|
|
r.mutex.Unlock()
|
|
return errors.New("no next segment")
|
|
}
|
|
r.segmentIndex++
|
|
r.mutex.Unlock()
|
|
return r.PrepareOutboundForLink(encrypt, sdu)
|
|
}
|
|
|
|
func (r *Resource) rawBodySizeLocked() (int64, error) {
|
|
src := r.data
|
|
if r.sourceData != nil {
|
|
src = r.sourceData
|
|
}
|
|
switch {
|
|
case src != nil:
|
|
return int64(len(src)), nil
|
|
case r.fileHandle != nil:
|
|
cur, err := r.fileHandle.Seek(0, io.SeekCurrent)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
end, err := r.fileHandle.Seek(0, io.SeekEnd)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if _, err := r.fileHandle.Seek(cur, io.SeekStart); err != nil {
|
|
return 0, err
|
|
}
|
|
return end, nil
|
|
default:
|
|
return 0, errors.New("no data")
|
|
}
|
|
}
|
|
|
|
func (r *Resource) readRawBodyLocked(offset, length int64) ([]byte, error) {
|
|
if length < 0 {
|
|
return nil, errors.New("negative read length")
|
|
}
|
|
if length == 0 {
|
|
return []byte{}, nil
|
|
}
|
|
src := r.data
|
|
if r.sourceData != nil {
|
|
src = r.sourceData
|
|
}
|
|
switch {
|
|
case src != nil:
|
|
if offset > int64(len(src)) {
|
|
return nil, errors.New("segment offset past end of data")
|
|
}
|
|
end := min(offset+length, int64(len(src)))
|
|
return append([]byte(nil), src[offset:end]...), nil
|
|
case r.fileHandle != nil:
|
|
if _, err := r.fileHandle.Seek(offset, io.SeekStart); err != nil {
|
|
return nil, err
|
|
}
|
|
buf := make([]byte, length)
|
|
n, err := io.ReadFull(r.fileHandle, buf)
|
|
if err == io.ErrUnexpectedEOF || err == io.EOF {
|
|
return buf[:n], nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return buf, nil
|
|
default:
|
|
return nil, errors.New("no data")
|
|
}
|
|
}
|
|
|
|
func (r *Resource) finishPrepareSegmentLocked(encrypt func([]byte) ([]byte, error), sdu int, segmentBody []byte, includeMeta bool) error {
|
|
keepOriginal := append([]byte(nil), r.originalHash...)
|
|
|
|
uncompressed := segmentBody
|
|
wireBody := uncompressed
|
|
if includeMeta && len(r.metadataPacked) > 0 {
|
|
wireBody = append(append([]byte(nil), r.metadataPacked...), uncompressed...)
|
|
}
|
|
|
|
randomHash := make([]byte, RandomHashSize)
|
|
if _, err := io.ReadFull(rand.Reader, randomHash); err != nil {
|
|
return err
|
|
}
|
|
|
|
payload := wireBody
|
|
if r.autoCompress {
|
|
compressed, err := bzip2CompressBody(wireBody)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(compressed) < len(wireBody) {
|
|
payload = compressed
|
|
r.compressed = true
|
|
} else {
|
|
r.compressed = false
|
|
}
|
|
} else {
|
|
r.compressed = false
|
|
}
|
|
|
|
hb := sha256.New()
|
|
hb.Write(wireBody)
|
|
hb.Write(randomHash)
|
|
r.hash = hb.Sum(nil)
|
|
r.randomHash = append([]byte(nil), randomHash...)
|
|
if len(keepOriginal) == sha256.Size {
|
|
r.originalHash = keepOriginal
|
|
} else {
|
|
r.originalHash = append([]byte(nil), r.hash...)
|
|
}
|
|
|
|
// ExpectedProof prepends metadataPacked when present on segment 1.
|
|
if r.split && r.sourceData == nil && r.data != nil {
|
|
r.sourceData = r.data
|
|
}
|
|
r.data = append([]byte(nil), segmentBody...)
|
|
|
|
plain := make([]byte, len(randomHash)+len(payload))
|
|
copy(plain, randomHash)
|
|
copy(plain[len(randomHash):], payload)
|
|
innerBlob, err := encrypt(plain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r.encrypted = true
|
|
|
|
partCount := (len(innerBlob) + sdu - 1) / sdu
|
|
if partCount > int(MaxSegments) {
|
|
return errors.New("resource too large")
|
|
}
|
|
r.segments = uint16(partCount) // #nosec G115
|
|
r.transferSize = int64(len(innerBlob))
|
|
r.dataSize = int64(len(wireBody))
|
|
|
|
r.hashmap = make([]byte, partCount*MapHashLen)
|
|
for i := range partCount {
|
|
start := i * sdu
|
|
end := min(start+sdu, len(innerBlob))
|
|
h := sha256.New()
|
|
h.Write(innerBlob[start:end])
|
|
h.Write(randomHash)
|
|
partHash := h.Sum(nil)
|
|
copy(r.hashmap[i*MapHashLen:], partHash[:MapHashLen])
|
|
}
|
|
|
|
r.outboundCipher = innerBlob
|
|
r.readOffset = 0
|
|
return nil
|
|
}
|
|
|
|
func (r *Resource) Read(p []byte) (n int, err error) {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
|
|
if r.outboundCipher != nil {
|
|
if r.readOffset >= int64(len(r.outboundCipher)) {
|
|
return 0, io.EOF
|
|
}
|
|
n = copy(p, r.outboundCipher[r.readOffset:])
|
|
r.readOffset += int64(n)
|
|
return n, nil
|
|
}
|
|
|
|
if r.data != nil {
|
|
if r.readOffset >= int64(len(r.data)) {
|
|
return 0, io.EOF
|
|
}
|
|
n = copy(p, r.data[r.readOffset:])
|
|
r.readOffset += int64(n)
|
|
return n, nil
|
|
}
|
|
|
|
if r.fileHandle != nil {
|
|
return r.fileHandle.Read(p)
|
|
}
|
|
|
|
return 0, errors.New("no data source available")
|
|
}
|
|
|
|
func (r *Resource) GetName() string {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return r.fileName
|
|
}
|
|
|
|
func (r *Resource) GetSize() int64 {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return r.dataSize
|
|
}
|
|
|
|
func (r *Resource) HasMetadata() bool {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return len(r.metadata) > 0 || len(r.metadataPacked) > 0
|
|
}
|
|
|
|
// SetMetadataPacked attaches pre-packed msgpack metadata (for integer keys).
|
|
func (r *Resource) SetMetadataPacked(packed []byte) error {
|
|
if r == nil {
|
|
return errors.New("nil resource")
|
|
}
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
if len(packed) == 0 {
|
|
r.metadata = nil
|
|
r.metadataPacked = nil
|
|
return nil
|
|
}
|
|
if len(packed) > MetadataMaxSize {
|
|
return errors.New("resource metadata size exceeded")
|
|
}
|
|
r.metadata = nil
|
|
blob := make([]byte, 3+len(packed))
|
|
n := len(packed)
|
|
blob[0] = byte(n >> 16) // #nosec G115 -- n bounded by MetadataMaxSize
|
|
blob[1] = byte(n >> 8) // #nosec G115 -- n bounded by MetadataMaxSize
|
|
blob[2] = byte(n) // #nosec G115 -- n bounded by MetadataMaxSize
|
|
copy(blob[3:], packed)
|
|
r.metadataPacked = blob
|
|
return nil
|
|
}
|
|
|
|
// SetMetadata attaches a metadata map transferred ahead of the file bytes.
|
|
// Keys and values must be msgpack-safe.
|
|
func (r *Resource) SetMetadata(meta map[string]any) error {
|
|
if r == nil {
|
|
return errors.New("nil resource")
|
|
}
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
if meta == nil {
|
|
r.metadata = nil
|
|
r.metadataPacked = nil
|
|
return nil
|
|
}
|
|
r.metadata = meta
|
|
r.metadataPacked = nil
|
|
return r.ensureMetadataPackedLocked()
|
|
}
|
|
|
|
// Metadata returns a shallow copy of the attached metadata map.
|
|
func (r *Resource) Metadata() map[string]any {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
if len(r.metadata) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]any, len(r.metadata))
|
|
maps.Copy(out, r.metadata)
|
|
return out
|
|
}
|
|
|
|
func (r *Resource) ensureMetadataPackedLocked() error {
|
|
if len(r.metadata) == 0 {
|
|
r.metadataPacked = nil
|
|
return nil
|
|
}
|
|
if len(r.metadataPacked) > 0 {
|
|
return nil
|
|
}
|
|
packed, err := msgpack.Marshal(r.metadata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(packed) > MetadataMaxSize {
|
|
return errors.New("resource metadata size exceeded")
|
|
}
|
|
// 3-byte big-endian length prefix (high 24 bits of a 32-bit length).
|
|
blob := make([]byte, 3+len(packed))
|
|
n := len(packed)
|
|
blob[0] = byte(n >> 16) // #nosec G115 -- n bounded by MetadataMaxSize
|
|
blob[1] = byte(n >> 8) // #nosec G115 -- n bounded by MetadataMaxSize
|
|
blob[2] = byte(n) // #nosec G115 -- n bounded by MetadataMaxSize
|
|
copy(blob[3:], packed)
|
|
r.metadataPacked = blob
|
|
return nil
|
|
}
|
|
|
|
func (r *Resource) IsRequest() bool {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return r.requestID != nil && !r.isResponse
|
|
}
|
|
|
|
func (r *Resource) IsResponse() bool {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return r.isResponse
|
|
}
|
|
|
|
func (r *Resource) GetRequestID() []byte {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
if r.requestID == nil {
|
|
return nil
|
|
}
|
|
return append([]byte{}, r.requestID...)
|
|
}
|
|
|
|
func (r *Resource) SetRequestID(id []byte) {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
if id == nil {
|
|
r.requestID = nil
|
|
return
|
|
}
|
|
r.requestID = append([]byte{}, id...)
|
|
}
|
|
|
|
func (r *Resource) SetIsResponse(isResponse bool) {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
r.isResponse = isResponse
|
|
}
|
|
|
|
func (r *Resource) getHashmap() []byte {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
if r.hashmap == nil {
|
|
return nil
|
|
}
|
|
return append([]byte{}, r.hashmap...)
|
|
}
|
|
|
|
// PartIndexForMapHash returns the part index whose map hash equals mh, or -1.
|
|
func (r *Resource) PartIndexForMapHash(mh []byte) int {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
if r.hashmap == nil || len(mh) != MapHashLen {
|
|
return -1
|
|
}
|
|
n := len(r.hashmap) / MapHashLen
|
|
for i := range n {
|
|
off := i * MapHashLen
|
|
if bytes.Equal(r.hashmap[off:off+MapHashLen], mh) {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// PartIndicesForMapHash returns all part indexes whose map hash equals mh.
|
|
func (r *Resource) PartIndicesForMapHash(mh []byte) []int {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
if r.hashmap == nil || len(mh) != MapHashLen {
|
|
return nil
|
|
}
|
|
n := len(r.hashmap) / MapHashLen
|
|
indexes := make([]int, 0, 1)
|
|
for i := range n {
|
|
off := i * MapHashLen
|
|
if bytes.Equal(r.hashmap[off:off+MapHashLen], mh) {
|
|
indexes = append(indexes, i)
|
|
}
|
|
}
|
|
return indexes
|
|
}
|
|
|
|
// OutboundCiphertextSlice returns the ciphertext bytes for part i using the given SDU.
|
|
func (r *Resource) OutboundCiphertextSlice(partIndex int, sdu int) []byte {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
if r.outboundCipher == nil || sdu <= 0 {
|
|
return nil
|
|
}
|
|
n := int(r.segments)
|
|
if partIndex < 0 || partIndex >= n {
|
|
return nil
|
|
}
|
|
start := partIndex * sdu
|
|
if start >= len(r.outboundCipher) {
|
|
return nil
|
|
}
|
|
end := min(start+sdu, len(r.outboundCipher))
|
|
out := make([]byte, end-start)
|
|
copy(out, r.outboundCipher[start:end])
|
|
return out
|
|
}
|
|
|
|
// OutboundCiphertextView returns a copy of outbound ciphertext for part i.
|
|
func (r *Resource) OutboundCiphertextView(partIndex int, sdu int) []byte {
|
|
return r.OutboundCiphertextSlice(partIndex, sdu)
|
|
}
|
|
|
|
// OutboundCiphertextSliceInto copies ciphertext bytes for part i into dst and returns dst.
|
|
// If dst capacity is insufficient, a new buffer is allocated.
|
|
func (r *Resource) OutboundCiphertextSliceInto(dst []byte, partIndex int, sdu int) []byte {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
if r.outboundCipher == nil || sdu <= 0 {
|
|
return nil
|
|
}
|
|
n := int(r.segments)
|
|
if partIndex < 0 || partIndex >= n {
|
|
return nil
|
|
}
|
|
start := partIndex * sdu
|
|
if start >= len(r.outboundCipher) {
|
|
return nil
|
|
}
|
|
end := min(start+sdu, len(r.outboundCipher))
|
|
need := end - start
|
|
if cap(dst) < need {
|
|
dst = make([]byte, need)
|
|
} else {
|
|
dst = dst[:need]
|
|
}
|
|
copy(dst, r.outboundCipher[start:end])
|
|
return dst
|
|
}
|
|
|
|
// MarkOutboundPartSent records that part i has been transmitted at least once.
|
|
// It returns true when every part index has been sent at least once.
|
|
func (r *Resource) MarkOutboundPartSent(i int) bool {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
n := int(r.segments)
|
|
if n == 0 {
|
|
return true
|
|
}
|
|
if i < 0 || i >= n {
|
|
return false
|
|
}
|
|
if r.outboundPartSent == nil {
|
|
r.outboundPartSent = make([]bool, n)
|
|
}
|
|
if !r.outboundPartSent[i] {
|
|
r.outboundPartSent[i] = true
|
|
r.outboundSentCount++
|
|
}
|
|
return r.outboundSentCount >= n
|
|
}
|
|
|
|
// IsOutboundPartSent reports whether part i has already been sent at least once.
|
|
func (r *Resource) IsOutboundPartSent(i int) bool {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
n := int(r.segments)
|
|
if i < 0 || i >= n || r.outboundPartSent == nil {
|
|
return false
|
|
}
|
|
return r.outboundPartSent[i]
|
|
}
|
|
|
|
// HashmapSegment returns a copy of the hashmap bytes for segment index.
|
|
func (r *Resource) HashmapSegment(linkMDU int, segment int) []byte {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
if segment < 0 || len(r.hashmap) == 0 {
|
|
return nil
|
|
}
|
|
entries := HashmapEntriesPerSegment(linkMDU)
|
|
if entries <= 0 {
|
|
entries = 1
|
|
}
|
|
totalEntries := len(r.hashmap) / MapHashLen
|
|
startEntry := segment * entries
|
|
if startEntry >= totalEntries {
|
|
return nil
|
|
}
|
|
endEntry := min(startEntry+entries, totalEntries)
|
|
start := startEntry * MapHashLen
|
|
end := endEntry * MapHashLen
|
|
out := make([]byte, end-start)
|
|
copy(out, r.hashmap[start:end])
|
|
return out
|
|
}
|
|
|
|
// MapHashAt returns the map hash at part index i.
|
|
func (r *Resource) MapHashAt(i int) []byte {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
if i < 0 {
|
|
return nil
|
|
}
|
|
off := i * MapHashLen
|
|
if off+MapHashLen > len(r.hashmap) {
|
|
return nil
|
|
}
|
|
out := make([]byte, MapHashLen)
|
|
copy(out, r.hashmap[off:off+MapHashLen])
|
|
return out
|
|
}
|
|
|
|
// OutboundTransferComplete reports whether every part has been sent at least once.
|
|
func (r *Resource) OutboundTransferComplete() bool {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return int(r.segments) > 0 && r.outboundSentCount >= int(r.segments)
|
|
}
|
|
|
|
func (r *Resource) GetRandomHash() []byte {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
if r.randomHash == nil {
|
|
return nil
|
|
}
|
|
return append([]byte{}, r.randomHash...)
|
|
}
|
|
|
|
// ExpectedProof returns SHA256(uncompressedPayload || resourceHash).
|
|
// When metadata is present on segment 1 the uncompressed payload is the
|
|
// metadata blob prepended to the file bytes.
|
|
func (r *Resource) ExpectedProof() ([]byte, bool) {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
if len(r.hash) != sha256.Size {
|
|
return nil, false
|
|
}
|
|
if r.data == nil {
|
|
return nil, false
|
|
}
|
|
body := r.data
|
|
if len(r.metadataPacked) > 0 && (r.segmentIndex == 0 || r.segmentIndex == 1) {
|
|
body = append(append([]byte(nil), r.metadataPacked...), r.data...)
|
|
}
|
|
sum := sha256.Sum256(append(append([]byte(nil), body...), r.hash...))
|
|
return sum[:], true
|
|
}
|
|
|
|
func (r *Resource) GetOriginalHash() []byte {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
if r.originalHash == nil {
|
|
return nil
|
|
}
|
|
return append([]byte{}, r.originalHash...)
|
|
}
|
|
|
|
func (r *Resource) GetSegmentIndex() uint16 {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return r.segmentIndex
|
|
}
|
|
|
|
func (r *Resource) GetTotalSegments() uint16 {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return r.totalSegments
|
|
}
|
|
|
|
func (r *Resource) IsEncrypted() bool {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return r.encrypted
|
|
}
|
|
|
|
func (r *Resource) IsSplit() bool {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
return r.split
|
|
}
|