Files
silo-server/internal/scanner/oshash.go

76 lines
2.1 KiB
Go

package scanner
import (
"encoding/binary"
"fmt"
"os"
)
const (
// oshashBlockSize is the number of bytes read from the start and end of a file.
oshashBlockSize = 65536
// oshashMinFileSize is the minimum file size required for OSHash (2 * blockSize).
oshashMinFileSize = oshashBlockSize * 2
// oshashUint64Count is the number of uint64 values in one block.
oshashUint64Count = oshashBlockSize / 8
)
// ComputeOSHash computes the OpenSubtitles hash of the file at the given path.
// The result is a 16-character lowercase hexadecimal string (zero-padded).
//
// Algorithm:
// 1. Open file, get size.
// 2. Read first 65536 bytes as array of uint64 (little-endian), sum them.
// 3. Read last 65536 bytes as array of uint64 (little-endian), sum them.
// 4. Add file size to the sum.
// 5. Format as 16-character lowercase hex (zero-padded).
//
// Files smaller than 128KB (65536 * 2) return an error.
func ComputeOSHash(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("oshash: open %s: %w", path, err)
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return "", fmt.Errorf("oshash: stat %s: %w", path, err)
}
fileSize := fi.Size()
if fileSize < oshashMinFileSize {
return "", fmt.Errorf("oshash: file %s is too small (%d bytes, minimum %d)", path, fileSize, oshashMinFileSize)
}
var hash uint64
// Read first 65536 bytes.
buf := make([]byte, oshashBlockSize)
if _, err := f.ReadAt(buf, 0); err != nil {
return "", fmt.Errorf("oshash: read head of %s: %w", path, err)
}
hash += sumUint64Block(buf)
// Read last 65536 bytes.
if _, err := f.ReadAt(buf, fileSize-oshashBlockSize); err != nil {
return "", fmt.Errorf("oshash: read tail of %s: %w", path, err)
}
hash += sumUint64Block(buf)
// Add file size.
hash += uint64(fileSize)
return fmt.Sprintf("%016x", hash), nil
}
// sumUint64Block interprets a byte buffer as an array of little-endian uint64
// values and returns their sum. The buffer length must be a multiple of 8.
func sumUint64Block(buf []byte) uint64 {
var sum uint64
for i := 0; i < len(buf); i += 8 {
sum += binary.LittleEndian.Uint64(buf[i : i+8])
}
return sum
}