108 lines
2.9 KiB
Go
108 lines
2.9 KiB
Go
package watcher
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base32"
|
|
"encoding/binary"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// MidHashSampleSize is the size of the sample to hash (1MB)
|
|
MidHashSampleSize = 1024 * 1024 // 1MB
|
|
|
|
// MidHash256Code is the custom multicodec for midhash256
|
|
MidHash256Code = 0x1000
|
|
)
|
|
|
|
// ComputeMidHash256 computes a midhash256 for a file.
|
|
//
|
|
// Algorithm:
|
|
// - For files <= 1MB: Hashes entire file content + size prefix
|
|
// - For files > 1MB: Hashes middle 1MB + size prefix
|
|
//
|
|
// The size prefix (8-byte big-endian) ensures that files with identical
|
|
// middle content but different sizes produce different hashes.
|
|
//
|
|
// Returns: CID v1 string in base32lower format (prefixed with "b")
|
|
func ComputeMidHash256(filePath string) (string, error) {
|
|
// Get file info
|
|
fileInfo, err := os.Stat(filePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
fileSize := fileInfo.Size()
|
|
|
|
// Open file
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer file.Close()
|
|
|
|
// Create size buffer (8-byte big-endian)
|
|
sizeBuffer := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(sizeBuffer, uint64(fileSize))
|
|
|
|
// Read sample data
|
|
var sampleData []byte
|
|
if fileSize <= MidHashSampleSize {
|
|
// Small file: read entire content
|
|
sampleData = make([]byte, fileSize)
|
|
_, err = io.ReadFull(file, sampleData)
|
|
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
|
return "", err
|
|
}
|
|
} else {
|
|
// Large file: read middle 1MB
|
|
middleOffset := (fileSize - MidHashSampleSize) / 2
|
|
_, err = file.Seek(middleOffset, io.SeekStart)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sampleData = make([]byte, MidHashSampleSize)
|
|
_, err = io.ReadFull(file, sampleData)
|
|
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
// Compute SHA-256 hash of [size + sample]
|
|
hasher := sha256.New()
|
|
hasher.Write(sizeBuffer)
|
|
hasher.Write(sampleData)
|
|
hashBytes := hasher.Sum(nil)
|
|
|
|
// Build CID v1 bytes:
|
|
// [version(0x01)] + [varint(codec 0x1000)] + [multihash]
|
|
// multihash = [varint(hashCode 0x1000)] + [varint(length 32)] + [hash bytes]
|
|
cidBytes := make([]byte, 0, 1+2+2+1+32)
|
|
|
|
// CID version 1
|
|
cidBytes = append(cidBytes, 0x01)
|
|
|
|
// Content codec as varint (0x1000 = 4096)
|
|
// varint encoding: 0x1000 = 0b0001_0000_0000_0000
|
|
// Split into 7-bit chunks: 0b00_0000_0000 (high) and 0b010_0000 (low with continuation)
|
|
// Result: [0x80, 0x20] for 0x1000
|
|
cidBytes = append(cidBytes, 0x80, 0x20)
|
|
|
|
// Multihash: hash function code as varint (0x1000)
|
|
cidBytes = append(cidBytes, 0x80, 0x20)
|
|
|
|
// Multihash: length as varint (32 = 0x20)
|
|
cidBytes = append(cidBytes, 0x20)
|
|
|
|
// Multihash: hash bytes
|
|
cidBytes = append(cidBytes, hashBytes...)
|
|
|
|
// Encode as base32lower (RFC 4648 without padding)
|
|
encoded := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(cidBytes)
|
|
encoded = strings.ToLower(encoded)
|
|
|
|
// Add multibase prefix "b" for base32lower
|
|
return "b" + encoded, nil
|
|
}
|