76 lines
2.1 KiB
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
|
|
}
|