package main
import (
"os"
"github.com/greycubesgav/integrity/pkg/integrity"
)
func main() {
status := integrity.Run()
os.Exit(status)
}
package integrity
import (
"crypto"
_ "embed"
"fmt"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"github.com/pborman/getopt/v2"
"golang.org/x/term"
)
type logLevel int
const (
logLevelPanic logLevel = iota
logLevelFatal
logLevelError
logLevelWarn
logLevelInfo
logLevelDebug
logLevelTrace
)
//go:embed docs/help.txt
var helpText string
//go:embed docs/usage.txt
var usageText string
const integrity_website = "https://github.com/greycubesgav/integrity"
const xattribute_name = "integrity"
const env_name_prefix = "INTEGRITY"
var digestTypes = map[string]crypto.Hash{
"md5": crypto.MD5,
"sha1": crypto.SHA1,
"sha224": crypto.SHA224,
"sha256": crypto.SHA256,
"sha384": crypto.SHA384,
"sha512": crypto.SHA512,
"sha3_224": crypto.SHA3_224,
"sha3_256": crypto.SHA3_256,
"sha3_384": crypto.SHA3_384,
"sha3_512": crypto.SHA3_512,
"sha512_224": crypto.SHA512_224,
"sha512_256": crypto.SHA512_256,
"blake2s_256": crypto.BLAKE2s_256,
"blake2b_256": crypto.BLAKE2b_256,
"blake2b_384": crypto.BLAKE2b_384,
"blake2b_512": crypto.BLAKE2b_512,
}
type Config struct {
ShowHelp bool
ShowVersion bool
ShowInfo bool
ShowUsage bool
showProgress bool
Verbose bool
Quiet bool
VerboseLevel int
DigestHash crypto.Hash
DigestName string
Action string
DisplayFormat string
Action_Add bool
Action_Delete bool
Action_List bool
Action_Transform bool
Action_Check bool
Option_Force bool
Option_ShortPaths bool
Option_Recursive bool
Option_AllDigests bool
xattribute_fullname string
xattribute_prefix string
logLevelName string
logLevel logLevel
returnCode int // used to store a return code for the cmd util
digestList map[string]crypto.Hash
digestNames []string
binaryDigestName string
isTerminal bool
}
// Logging function, only outputs if the log level is less than or equal to the current log level
func (c *Config) log(level string, format string, args ...interface{}) {
var logLevel logLevel
switch level {
case "panic":
logLevel = logLevelPanic
case "fatal":
logLevel = logLevelFatal
case "error":
logLevel = logLevelError
case "warn":
logLevel = logLevelWarn
case "info":
logLevel = logLevelInfo
case "debug":
logLevel = logLevelDebug
case "trace":
logLevel = logLevelTrace
default:
logLevel = logLevelInfo
}
if logLevel <= c.logLevel {
if logLevel <= 2 {
fmt.Fprintf(os.Stderr, format, args...)
} else {
fmt.Fprintf(os.Stdout, format, args...)
}
}
}
func newConfig() *Config {
var c *Config = &Config{
ShowHelp: false,
ShowVersion: false,
ShowInfo: false,
ShowUsage: false,
showProgress: false,
Action_Check: false,
Action_Add: false,
Action_Delete: false,
Action_List: false,
Action_Transform: false,
Option_Force: false,
Option_ShortPaths: false,
Option_Recursive: false,
Option_AllDigests: false,
Verbose: false,
Quiet: false,
VerboseLevel: 1,
DigestHash: crypto.SHA1,
DigestName: "",
DisplayFormat: "",
Action: "check",
xattribute_fullname: "",
xattribute_prefix: "",
logLevelName: "info",
logLevel: logLevelInfo,
returnCode: 0,
digestList: make(map[string]crypto.Hash),
digestNames: make([]string, 0),
binaryDigestName: "",
isTerminal: term.IsTerminal(int(os.Stdout.Fd())),
}
c.parseCmdlineOpt()
return c
}
func (c *Config) parseCmdlineOpt() {
var userDigestString string
// Set the potential command line options
getopt.FlagLong(&c.ShowHelp, "help", 'h', "show this help")
getopt.FlagLong(&c.ShowVersion, "version", 0, "show version")
getopt.FlagLong(&c.ShowInfo, "info", 0, "show information")
getopt.FlagLong(&c.ShowUsage, "usage", 0, "show command flag usage")
getopt.FlagLong(&c.Action_Check, "check", 'c', "check the checksum of the file matches the one stored in the extended attributes [default]")
getopt.FlagLong(&c.Action_Add, "add", 'a', "calculate the checksum of the file and add it to the extended attributes")
getopt.FlagLong(&c.Action_Delete, "delete", 'd', "delete a checksum stored for a file")
getopt.FlagLong(&c.Action_List, "list", 'l', "list the checksum stored for a file")
getopt.FlagLong(&c.Action_Transform, "fix-old", 0, "fix an old extended attribute value name to the current format")
getopt.FlagLong(&c.Option_AllDigests, "all", 'x', "include all digests, not just the default digest. Only applies to --delete and --list options")
getopt.FlagLong(&c.Option_Force, "force", 'f', "force the calculation and writing of a checksum even if one already exists (default behaviour is to skip files with checksums already stored)")
getopt.FlagLong(&c.showProgress, "progress", 'p', "show the progress of each file checksum calculation")
getopt.FlagLong(&c.Verbose, "verbose", 'v', "output more information.")
getopt.FlagLong(&c.Quiet, "quiet", 'q', "output less information.")
getopt.FlagLong(&c.logLevelName, "loglevel", 0, "set the logging level. One of: panic, fatal, error, warn, info, debug, trace.")
getopt.FlagLong(&userDigestString, "digest", 0, "set the digest method(s) as a comma separated list (see help for list of digest types available)")
getopt.FlagLong(&c.Option_ShortPaths, "short-paths", 's', "show only file name when showing file names, useful for generating sha1sum files")
getopt.FlagLong(&c.Option_Recursive, "recursive", 'r', "recurse into sub-directories")
getopt.FlagLong(&c.DisplayFormat, "display-format", 0, "set the output display format (sha1sum, md5sum). Note: this only shows any checkfiles ")
getopt.Parse()
//-----------------------------------------------------------------------------------------
// Cover the help displays with exits first
//-----------------------------------------------------------------------------------------
// Show simple help, note return code '1' resvered for exit app but return '0' to terminal
if c.ShowHelp {
printHelp()
c.returnCode = 1 // Show help
return
}
// Show the version of the app and exit
if c.ShowVersion {
fmt.Printf("%s\n", integrity_version)
c.returnCode = 1 // Show version
return
}
// Show just the command flags and exit
if c.ShowUsage {
getopt.Usage()
c.returnCode = 1 // Show info
return
}
//-----------------------------------------------------------------------------------------
// Return error of no arguments are given
//-----------------------------------------------------------------------------------------
if getopt.NArgs() == 0 && !c.ShowInfo {
fmt.Fprint(os.Stderr, "Error : no arguments given\n")
getopt.Usage()
c.returnCode = 2 // No arguments
return
}
//-----------------------------------------------------------------------------------------
// Setup the logging level
//-----------------------------------------------------------------------------------------
if c.logLevelName == "trace" {
c.logLevel = logLevelTrace
} else if c.logLevelName == "debug" {
c.logLevel = logLevelDebug
} else if c.logLevelName == "info" {
c.logLevel = logLevelInfo
} else if c.logLevelName == "warn" {
c.logLevel = logLevelWarn
} else if c.logLevelName == "fatal" {
c.logLevel = logLevelFatal
} else if c.logLevelName == "panic" {
c.logLevel = logLevelPanic
} else {
c.logLevel = logLevelInfo
}
c.log("debug", "LogObjectlevel : [%d]\n", c.logLevel)
//-----------------------------------------------------------------------------------------
// Main Actions
//-----------------------------------------------------------------------------------------
if c.Action_Check {
c.Action = "check"
} else if c.Action_Delete {
c.Action = "delete"
} else if c.Action_Add {
c.Action = "add"
} else if c.Action_List {
c.Action = "list"
} else if c.Action_Transform {
c.Action = "transform"
}
c.log("debug", "c.Action: '%s'\n", c.Action)
//-----------------------------------------------------------------------------------------
// Workout the digest we are using
// Hierarchy
// 1. binary name , e.g. integriy.sha1
// └ 2. command line all digest option
// └ 3. command line option, e.g. --digest=sha256,sha512
// └ 4. environment variable, e.g. INTEGRITY_DIGEST='md5,sha256'
// Note: the binary name overwrites any other options
//-----------------------------------------------------------------------------------------
// Output of this block is a list of potential digestNames, will be validated later
if cmdHash := strings.Split(filepath.Base(os.Args[0]), "."); len(cmdHash) == 2 {
// Try and get the digest from the name of the binary, e.g integriy.sha1, integrity.md5
// this overrides any other digest setting, other than display formats sha1sum, md5sum etc
c.digestNames = []string{cmdHash[1]}
c.binaryDigestName = cmdHash[1]
} else if c.Option_AllDigests {
// Otherwise, if we've been asked to perform against all digest types
for digestName := range digestTypes {
c.digestNames = append(c.digestNames, digestName)
}
// Add the two digests that don't come from crypto.Hash
c.digestNames = append(c.digestNames, "oshash")
c.digestNames = append(c.digestNames, "phash")
} else {
// If we've not been given a string from the user, try and get it from the environment
if userDigestString == "" {
userDigestString = os.Getenv(env_name_prefix + "_DIGEST")
}
userDigestArray := strings.Split(userDigestString, ",")
if len(userDigestArray) == 1 && userDigestArray[0] == "" {
// If all this fails to set the digest, we'll default to sha1
c.digestNames = []string{"sha1"}
} else {
c.digestNames = append(c.digestNames, userDigestArray...)
}
}
// Check if the display format doesn't make the digest
if c.DisplayFormat != "" {
c.log("debug", "c.DisplayFormat: '%s'\n", c.DisplayFormat)
// Either we override the action to be list, or we error that the action is not list
// if c.Action != "list" {
// fmt.Fprintf(os.Stderr, "Error : Display format provided but not performing list '%s'\n Should be one of: sha1sum, md5sum\n", c.DisplayFormat)
// c.returnCode = 8 // Display format provided but action not list
// return
// }
// Override the action to be list as we've been given a display format
c.Action = "list"
// Validate the display format
switch c.DisplayFormat {
case "sha1sum":
if c.binaryDigestName != "" && c.binaryDigestName != "sha1" {
c.log("error", "Error : asked for sha1sum output but not sha1 binary.\n")
c.returnCode = 6 // sha1sum output but not .md5 binary
return
}
c.digestNames = []string{"sha1"}
case "md5sum":
if c.binaryDigestName != "" && c.binaryDigestName != "md5" {
c.log("error", "Error : asked for md5sum output but not md5 binary.\n")
c.returnCode = 7 // md5sum output but not .md5 binary
return
}
c.digestNames = []string{"md5"}
case "cksum":
// We will output any checksum in this case, no need to force the digest
default:
c.log("error", "Error : unknown display format '%s'\n Should be one of: sha1sum, md5sum\n", c.DisplayFormat)
c.returnCode = 4 // Unknown display format
return
}
}
c.log("debug", "c.digestNames: '%s'\n", c.digestNames)
//-----------------------------------------------------------------------------------------
// Check we know all the given digest names
//-----------------------------------------------------------------------------------------
for _, digestName := range c.digestNames {
if digestName != "oshash" && digestName != "phash" {
if digest, exists := digestTypes[digestName]; exists {
c.digestList[digestName] = digest
} else {
c.log("error", "Error : unknown digest type '%s'\n", digestName)
c.returnCode = 5 // Unknown digest
return
}
}
}
// Sort the file list to aid printing
sort.Strings(c.digestNames)
// Check the current OS and create the full xattribute name from the os, const and digest
switch runtime.GOOS {
case "darwin", "freebsd":
c.xattribute_prefix = fmt.Sprintf("%s.", xattribute_name)
case "linux":
c.xattribute_prefix = fmt.Sprintf("user.%s.", xattribute_name)
default:
c.log("error", "Error: non-supported OS type '%s'\nSupported OS types 'darwin, freebsd, linux'\n", runtime.GOOS)
c.returnCode = 3 // Unknown OS
return
}
c.log("debug", "c.xattribute_prefix: '%s'\n", c.xattribute_prefix)
// Show internal info about the apps
if c.ShowInfo {
c.log("info", "integrity version: %s\nintegrity attribute prefix: %s\nruntime environment: %s\ndigest list: %s\nintegrity verbose level: %d\n", integrity_version, c.xattribute_prefix, runtime.GOOS, c.digestNames, c.VerboseLevel)
c.returnCode = 1 // Show info
return
}
if c.Quiet {
c.VerboseLevel = 0
} else if c.Verbose {
c.VerboseLevel = 2
} else {
c.VerboseLevel = 1
}
}
func printHelp() {
fmt.Printf("integrity version %s\n", integrity_version)
fmt.Printf("Web site: %s\n", integrity_website)
fmt.Println(helpText)
//getopt.Usage()
getopt.PrintUsage(os.Stdout)
fmt.Println("Usage Examples:")
fmt.Println(usageText)
}
package integrity
import (
_ "crypto/md5"
_ "crypto/sha1"
_ "crypto/sha256"
_ "crypto/sha512"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/pborman/getopt/v2"
"github.com/pkg/xattr"
_ "golang.org/x/crypto/blake2b"
_ "golang.org/x/crypto/blake2s"
_ "golang.org/x/crypto/sha3"
)
type integrity_fileCard struct {
FileInfo *os.FileInfo
fullpath string
checksum string
digest_name string
}
// Buffer size for reading from file to show progress
const fileBufferSize = 1024 * 1024 // 1MB
// ToDo Add option to skip mac files http://www.westwind.com/reference/OS-X/invisibles.html
// ToDo change errors to summarise at end like rsync - some errors occurred
// ToDo check all errors goto stderr all normal messages go to stdout
// Global config structure used througout the code
var config *Config = nil
func integ_testChecksumStored(currentFile *integrity_fileCard) (bool, error) {
var err error
if _, err = xattr.Get(currentFile.fullpath, config.xattribute_fullname); err != nil {
var errorString string = err.Error()
if strings.Contains(errorString, "attribute not found") || strings.Contains(errorString, "no data available") {
// We got an error with attribute not found (darwin) or no data available (linux) so simply return false and no error
return false, nil
} else {
// We got a different error so return false and the error
return false, err
}
}
// We must have an attribute stored
return true, nil
}
func integ_swapXattrib(currentFile *integrity_fileCard) error {
// ToDo: add new custom error for cases where none of the old names were found
// Outout != RENAMED => SKIPPED
var err error
var data []byte
var found bool = false
attributeNames := []string{"user.integ.sha1", "integ.sha1", "user.integrity.sha1"}
for _, oldAttribute := range attributeNames {
if runtime.GOOS == "linux" {
oldAttribute = "user." + oldAttribute
}
data, err = xattr.Get(currentFile.fullpath, oldAttribute)
if err != nil {
var errorString string = err.Error()
if strings.Contains(errorString, "attribute not found") || strings.Contains(errorString, "no data available") {
switch config.VerboseLevel {
case 0, 1:
// Don't print anything we're 'quiet'
case 2:
displayFileMessageNoDigest(currentFile.fullpath, fmt.Sprintf("old attribute not found : %s", oldAttribute))
}
} else {
// We got a different error looking for the attribute
return err
}
} else {
// We must have found an old attribute
found = true
switch config.VerboseLevel {
case 0, 1:
// Don't print anything we're 'quiet'
case 2:
displayFileMessageNoDigest(currentFile.fullpath, fmt.Sprintf("Found old attribute [%s] : Setting new attribute: [%s]", oldAttribute, config.xattribute_fullname))
}
if err = xattr.Set(currentFile.fullpath, config.xattribute_fullname, data); err != nil {
return err
}
if err = xattr.Remove(currentFile.fullpath, oldAttribute); err != nil {
return err
}
}
}
if !found {
// We've not found any of the old attributes
err = errors.New("no old attributes found")
return err
}
return nil
}
func integ_getChecksumRaw(path string) (string, error) {
var err error
var data []byte
if data, err = xattr.Get(path, config.xattribute_fullname); err != nil {
return "", err
}
return string(data), nil
}
func integ_getChecksum(currentFile *integrity_fileCard) error {
var err error
currentFile.digest_name = config.DigestName
if currentFile.checksum, err = integ_getChecksumRaw(currentFile.fullpath); err != nil {
return err
}
return nil
}
// integ_removeChecksum tries to remove a defined checksum attribute
// if we get an error because the attribute didn't exist we suppress the error and simple return false
// if we get an other type of error we pass it back
// otherwise we assume all is well and return true
// this allows the outer code to determine if we actually removed an attribute or not
func integ_removeChecksum(currentFile *integrity_fileCard) (bool, error) {
var err error
if err = xattr.Remove(currentFile.fullpath, config.xattribute_fullname); err != nil {
var errorString string = err.Error()
if strings.Contains(errorString, "attribute not found") || strings.Contains(errorString, "no data available") {
// We got an error with attribute not found so simply return false and no error
return false, nil
} else {
// We got a different error so return false and the error
return false, err
}
}
// We must have removed the attribute
return true, nil
}
func integ_generateChecksum(currentFile *integrity_fileCard) error {
var err error
fileHandle, err := os.Open(currentFile.fullpath)
if err != nil {
return err
}
// Add a function to defer to ensure any issue closing the file is reported
defer func() {
if err := fileHandle.Close(); err != nil {
// We don't use config.log here as we want to ensure this is a simple as possible
fmt.Fprintf(os.Stderr, "Error closing file: %s\n", err)
}
}()
config.log("debug", "integ_generateChecksum config.DigestName:%s\n", config.DigestName)
if config.DigestName == "oshash" {
currentFile.checksum, err = oshashFromFilePath(currentFile.fullpath)
if err != nil {
return err
}
} else if config.DigestName == "phash" {
currentFile.checksum, err = integrityPhashFromFile(currentFile.fullpath)
if err != nil {
return err
}
} else {
hashObj := config.digestList[config.DigestName]
if !hashObj.Available() {
config.log("debug", "integ_generateChecksum !hashObj.Available():%s\n", config.DigestName)
return fmt.Errorf("integ_generateChecksum: hash object [%s] not supported", hashObj)
}
hashFunc := hashObj.New()
// If we're showing a progress bar, we write in chunks of 1MB
if config.showProgress {
fileInfo := *currentFile.FileInfo
readBuffer := make([]byte, fileBufferSize)
var fileTotalBytesRead int64 = 0
var filePercentageRead int64
// If we are being piped to another command we output newlines instead of rewriting line
var returnChar = '\r'
if !config.isTerminal {
returnChar = '\n'
}
for {
// Read a chunk of the file
n, err := fileHandle.Read(readBuffer)
if err != nil && err != io.EOF {
return err
} else if err == io.EOF {
break
}
// Write the data chunk to the hash
_, err = hashFunc.Write(readBuffer[:n])
if err != nil {
return err
}
// Update total bytes read
fileTotalBytesRead += int64(n)
// Percentage complete
filePercentageRead = fileTotalBytesRead * 100 / fileInfo.Size()
// Output progress, regardless of the verbosity level
fmt.Printf("%s : read : %d%%%c", currentFile.fullpath, filePercentageRead, returnChar)
}
// Return to start of line to overwrite percentage line
if config.isTerminal {
fmt.Printf("\r")
}
} else {
// If we're not showing a progress bar, we read the whole file and write it to the hash
if _, err := io.Copy(hashFunc, fileHandle); err != nil {
return err
}
}
currentFile.checksum = hex.EncodeToString(hashFunc.Sum(nil))
}
config.log("debug", "integ_generateChecksum currentFile.checksum:%s\n", currentFile.checksum)
return nil
}
func integ_addChecksum(currentFile *integrity_fileCard) error {
var err error
// Write a new checksum to the file
if err = integ_writeChecksum(currentFile); err != nil {
return err
}
// Confirm that the checksum written to the xatrib when read back matches the one in memory
if err = integ_confirmChecksum(currentFile, currentFile.checksum); err != nil {
return err
}
return nil
}
func integ_writeChecksum(currentFile *integrity_fileCard) error {
var err error
if err = integ_generateChecksum(currentFile); err != nil {
return err
}
checksumBytes := []byte(currentFile.checksum)
if err = xattr.Set(currentFile.fullpath, config.xattribute_fullname, checksumBytes); err != nil {
return err
}
return nil
}
func integ_confirmChecksum(currentFile *integrity_fileCard, testChecksum string) error {
var err error
var xtattrbChecksum string
if xtattrbChecksum, err = integ_getChecksumRaw(currentFile.fullpath); err != nil {
return err
}
if testChecksum != xtattrbChecksum {
return fmt.Errorf("calculated checksum and filesystem read checksum differ!\n ├── stored [%s]\n └── calc'd [%s]", xtattrbChecksum, currentFile.checksum)
}
currentFile.digest_name = config.DigestName
return nil
}
func integ_checkChecksum(currentFile *integrity_fileCard) error {
var err error
// Generate the checksum from the file contents
// Stores the generated checksum in currentFile.checksum
if err = integ_generateChecksum(currentFile); err != nil {
return err
}
// Check the checksum using the current file and the checksum just generated previously
if err = integ_confirmChecksum(currentFile, currentFile.checksum); err != nil {
return err
}
return nil
}
func integ_printChecksum(currentFile *integrity_fileCard, fileDisplayPath string) error {
// Pass in the fileDisplayPath so we only need to generate it once outside this function
var err error
if err = integ_getChecksum(currentFile); err != nil {
var errorString string = err.Error()
// Two different errors can be returned depending on the OS
if strings.Contains(errorString, "attribute not found") || strings.Contains(errorString, "no data available") {
switch config.VerboseLevel {
case 0:
// Don't print anything we're 'quiet'
case 1:
displayFileMessage(fileDisplayPath, "[none]")
case 2:
displayFileMessage(fileDisplayPath, fmt.Sprintf("[no checksum stored in %s]", config.xattribute_fullname))
}
} else {
switch config.VerboseLevel {
case 0, 1:
// Always output errors if we're 'quiet'
displayFileErrorMessage(fileDisplayPath, "FAILED")
case 2:
displayFileErrorMessage(fileDisplayPath, fmt.Sprintf("FAILED : Error reading checksum : %s", err.Error()))
}
return err
}
} else {
displayFileMessage(fileDisplayPath, currentFile.checksum)
}
return nil
}
func displayFileMessageNoDigest(fileDisplayPath string, message string) {
fmt.Printf("%s : %s\n", fileDisplayPath, message)
}
func displayFileErrorMessageNoDigest(fileDisplayPath string, message string) {
fmt.Fprintf(os.Stderr, "%s : %s\n", fileDisplayPath, message)
}
func displayFileMessage(fileDisplayPath string, message string) {
if config.DisplayFormat == "sha1sum" && strings.HasPrefix(config.DigestName, "sha") {
fmt.Printf("%s *%s\n", message, fileDisplayPath)
} else if config.DisplayFormat == "md5sum" && strings.HasPrefix(config.DigestName, "md5") {
fmt.Printf("%s %s\n", message, fileDisplayPath)
} else if config.DisplayFormat == "cksum" {
fmt.Printf("%s (%s) = %s\n", config.DigestName, fileDisplayPath, message)
} else {
fmt.Printf("%s : %s : %s\n", fileDisplayPath, config.DigestName, message)
}
}
func displayFileErrorMessage(fileDisplayPath string, message string) {
fmt.Fprintf(os.Stderr, "%s : %s : %s\n", fileDisplayPath, config.DigestName, message)
}
func integ_generatefileDisplayPath(currentFile *integrity_fileCard) string {
if config.Option_ShortPaths {
fileInfo := *currentFile.FileInfo
return fileInfo.Name()
} else {
return currentFile.fullpath
}
}
func Run() int {
config = newConfig()
config.log("debug", "integrity.Run()\n")
config.log("debug", "config.returnCode: %d\n", config.returnCode)
switch config.returnCode {
case 0:
// config.returnCode=0 reserved for success
case 1:
// config.returnCode=1 reserved for show help runs, we show output and then exit but it wasn't an error
return 0
default:
return config.returnCode
}
for _, path := range getopt.Args() {
// ToDo: Consider how to deal with symlinks, should be follow them?
config.log("debug", "path: '%s'\n", path)
path_fileinfo, err := os.Stat(path)
// If we can stat the given file
if err != nil {
errorString := err.Error()
if strings.Contains(errorString, "no such file or directory") {
config.log("error", "%s : no such file or directory\n", path)
config.returnCode = 10 // No such file or directory
continue
}
displayFileErrorMessageNoDigest(path, fmt.Sprintf("ERROR : %s", err.Error()))
config.returnCode = 12 // Error stating file
continue
}
if path_fileinfo.IsDir() {
config.log("debug", "path is directory: recurse? '%t'\n", config.Option_Recursive)
if config.Option_Recursive {
// Walk the directory structure
err := filepath.Walk(path, handle_path)
if err != nil {
config.log("debug", "Error from filepath.Walk: err(%s)", err.Error())
return 1
}
} else {
switch config.VerboseLevel {
case 0, 1:
// Don't print anything we're 'quiet' / this is not an error
case 2:
displayFileMessageNoDigest(path, "skipping directory")
}
}
} else {
if err = handle_path(path, path_fileinfo, err); err != nil {
displayFileErrorMessageNoDigest(path, fmt.Sprintf("ERROR : %s", err.Error()))
config.returnCode = 13 // Error handling path
continue
}
}
}
config.log("debug", "config.returnCode: %d\n", config.returnCode)
return config.returnCode
}
func handle_path(path string, fileinfo os.FileInfo, err error) error {
config.log("debug", "handle_path: '%s'\n", path)
if err != nil {
config.log("debug", "handle_path: error '%s'\n", err)
if strings.Contains(err.Error(), "permission denied") {
switch config.VerboseLevel {
case 0, 1:
// Always output errors even if we're 'quiet'
displayFileErrorMessageNoDigest(path, "skipped")
case 2:
displayFileErrorMessageNoDigest(path, fmt.Sprintf("skipped : %s", err.Error()))
}
return filepath.SkipDir
} else {
// Handle the error and return it to stop walking
config.log("error", "Error walking the path : %v : %v\n", path, err)
return err
}
}
config.log("debug", "no errors continuing\n")
if !fileinfo.IsDir() {
var currentFile integrity_fileCard
currentFile.FileInfo = &fileinfo
currentFile.fullpath = path
// Generate the display path here as most options will need it
var fileDisplayPath string = integ_generatefileDisplayPath(¤tFile)
switch config.Action {
case "list":
for _, digestName := range config.digestNames {
config.DigestName = digestName
config.xattribute_fullname = config.xattribute_prefix + config.DigestName
config.log("debug", "list: '%s'\n", config.xattribute_fullname)
if err = integ_printChecksum(¤tFile, fileDisplayPath); err != nil {
// Only continue as the function would have printed any error already
continue
}
}
case "delete":
for _, digestName := range config.digestNames {
config.DigestName = digestName
config.xattribute_fullname = config.xattribute_prefix + config.DigestName
config.log("debug", "delete: '%s'\n", config.xattribute_fullname)
hadAttribute, err := integ_removeChecksum(¤tFile)
if err != nil {
switch config.VerboseLevel {
case 0, 1:
// Always output errors if we're 'quiet'
displayFileErrorMessage(fileDisplayPath, "FAILED")
case 2:
displayFileErrorMessage(fileDisplayPath, fmt.Sprintf("FAILED : Error removing checksum : %s", err.Error()))
}
} else if !hadAttribute {
switch config.VerboseLevel {
case 0:
// Don't print anything we're 'quiet'
// Does this make sense here? Do we want to still print this error if we're 'quiet'?
case 1:
displayFileMessage(fileDisplayPath, "no attribute")
case 2:
displayFileMessage(fileDisplayPath, "no checksum attribute found")
}
} else {
switch config.VerboseLevel {
case 0:
// Don't print anything we're 'quiet'
case 1:
displayFileMessage(fileDisplayPath, "removed")
case 2:
displayFileMessage(fileDisplayPath, "removed checksum attribute")
}
}
}
case "add":
for _, digestName := range config.digestNames {
config.DigestName = digestName
config.xattribute_fullname = config.xattribute_prefix + config.DigestName
config.log("debug", "add: '%s'\n", config.xattribute_fullname)
if !config.Option_Force {
var haveDigestStored bool
haveDigestStored, err = integ_testChecksumStored(¤tFile)
if err != nil {
switch config.VerboseLevel {
case 0, 1:
// Always output errors even if we're 'quiet'
displayFileErrorMessage(fileDisplayPath, "FAILED")
case 2:
displayFileErrorMessage(fileDisplayPath, fmt.Sprintf("FAILED : Error testing for existing checksum : %s", err.Error()))
}
return nil
} else if haveDigestStored {
switch config.VerboseLevel {
case 0:
// Don't print anything we're 'quiet'
case 1:
displayFileMessage(fileDisplayPath, "skipped")
case 2:
displayFileMessage(fileDisplayPath, "skipped : We already have a checksum stored")
}
continue
}
}
// If we've reached here we must want to add the checksum
if err = integ_addChecksum(¤tFile); err != nil {
switch config.VerboseLevel {
case 0, 1:
// Always output errors even if we're 'quiet'
displayFileErrorMessage(fileDisplayPath, "FAILED")
case 2:
displayFileErrorMessage(fileDisplayPath, fmt.Sprintf("FAILED : Error adding checksum : %s", err.Error()))
}
} else {
switch config.VerboseLevel {
case 0:
// Don't print anything we're 'quiet'
case 1:
displayFileMessage(fileDisplayPath, "added")
case 2:
displayFileMessage(fileDisplayPath, fmt.Sprintf("%s : added", currentFile.checksum))
}
}
}
case "check":
for _, digestName := range config.digestNames {
config.DigestName = digestName
config.xattribute_fullname = config.xattribute_prefix + config.DigestName
config.log("debug", "check: '%s'\n", config.xattribute_fullname)
var haveDigestStored bool
if haveDigestStored, err = integ_testChecksumStored(¤tFile); err != nil {
switch config.VerboseLevel {
case 0, 1:
// Always output errors even if we're 'quiet'
displayFileErrorMessage(fileDisplayPath, "FAILED")
case 2:
displayFileErrorMessage(fileDisplayPath, fmt.Sprintf("FAILED : failed checking if checksum was stored : %s", err.Error()))
}
return nil
} else {
if haveDigestStored {
if err = integ_checkChecksum(¤tFile); err != nil {
switch config.VerboseLevel {
case 0, 1:
// Always output errors even if we're 'quiet'
displayFileErrorMessage(fileDisplayPath, "FAILED")
case 2:
displayFileErrorMessage(fileDisplayPath, fmt.Sprintf("FAILED : %s", err.Error()))
}
} else {
switch config.VerboseLevel {
case 0:
// Don't print anything we're 'quiet'
case 1:
displayFileMessage(fileDisplayPath, "PASSED")
case 2:
displayFileMessage(fileDisplayPath, fmt.Sprintf("%s : PASSED", currentFile.checksum))
}
}
} else {
switch config.VerboseLevel {
case 0:
// Musing: is it an 'error' if we don't have a checksum?
// Answer: "no" We have 2 states for no output during check,
// The file has a checksum and it is correct or it doesn't have a checksum
// The assumption here is if we are quiet and don't have a checksum the file
// isn't important enough to check
case 1:
displayFileMessage(fileDisplayPath, "no checksum")
case 2:
displayFileMessage(fileDisplayPath, "no checksum, skipped")
}
return nil
}
}
}
case "transform":
if err = integ_swapXattrib(¤tFile); err != nil {
errorString := err.Error()
if strings.Contains(errorString, "no old attributes found") {
switch config.VerboseLevel {
case 0:
// Don't print anything we're 'quiet'
case 1:
displayFileMessageNoDigest(fileDisplayPath, "skipped")
case 2:
displayFileMessageNoDigest(fileDisplayPath, "skipped : No old attributes found")
}
} else {
switch config.VerboseLevel {
case 0:
// Always output errors even if we're 'quiet'
displayFileErrorMessage(fileDisplayPath, "ERROR")
case 1:
displayFileErrorMessage(fileDisplayPath, "ERROR : Error renaming checksum")
case 2:
displayFileErrorMessage(fileDisplayPath, fmt.Sprintf("ERROR : Error renaming checksum : %s", err.Error()))
}
}
} else {
switch config.VerboseLevel {
case 0:
// Don't print anything we're 'quiet'
case 1:
displayFileMessage(fileDisplayPath, "RENAMED")
case 2:
displayFileMessage(fileDisplayPath, "RENAMED : Renamed any old integrity attributes")
}
}
default:
config.log("error", "Error : Unknown action \"%s\"\n", config.Action)
config.returnCode = 9 // Unknown action
return errors.New("unknown action")
}
}
return nil
}
package integrity
import (
"bytes"
"encoding/binary"
"fmt"
"os"
)
// oshashFromFilePath calculates the hash using the same algorithm that
// OpenSubtitles.org uses.
// https://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes
//
// Calculation is as follows:
// size + 64 bit checksum of the first and last 64k bytes of the file.
func oshashFromFilePath(filePath string) (string, error) {
f, err := os.Open(filePath)
if err != nil {
return "", err
}
// Add a function to defer to ensure any issue closing the file is reported
defer func() {
if err := f.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Error closing file: %s\n", err)
}
}()
fi, err := f.Stat()
if err != nil {
return "", err
}
fileSize := int64(fi.Size())
if fileSize == 0 {
return "", nil
}
const chunkSize = 64 * 1024
fileChunkSize := int64(chunkSize)
if fileSize < fileChunkSize {
fileChunkSize = fileSize
}
head := make([]byte, fileChunkSize)
tail := make([]byte, fileChunkSize)
// read the head of the file into the start of the buffer
_, err = f.Read(head)
if err != nil {
return "", err
}
// seek to the end of the file - the chunk size
_, err = f.Seek(-fileChunkSize, 2)
if err != nil {
return "", err
}
// read the tail of the file
_, err = f.Read(tail)
if err != nil {
return "", err
}
// put the head and tail together
buf := append(head, tail...)
// convert bytes into uint64
ints := make([]uint64, len(buf)/8)
reader := bytes.NewReader(buf)
err = binary.Read(reader, binary.LittleEndian, &ints)
if err != nil {
return "", err
}
// sum the integers
var sum uint64
for _, v := range ints {
sum += v
}
// add the filesize
sum += uint64(fileSize)
// output as hex
return fmt.Sprintf("%016x", sum), nil
}
package integrity
import (
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
"os"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
"github.com/corona10/goimagehash"
)
func integrityPhashFromFile(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
// Add a function to defer to ensure any issue closing the file is reported
defer func() {
if err := file.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Error closing file: %s\n", err)
}
}()
fileInfo, err := file.Stat()
if err != nil {
return "", err
}
fileSize := int64(fileInfo.Size())
if fileSize == 0 {
return "", fmt.Errorf("filesize is zero")
}
// Limit the reader to only the necessary bytes
limitedReader := io.LimitReader(file, 512) // 512 bytes should be enough for most formats
// Use image.DecodeConfig to only decode the configuration (which includes format)
_, _, err = image.DecodeConfig(limitedReader)
if err != nil {
return "", err
}
// Check the image type
buf := make([]byte, 512)
_, err = file.Read(buf)
if err != nil {
return "", err
}
// Reset the file pointer
_, err = file.Seek(0, io.SeekStart)
if err != nil {
return "", err
}
// Decode the entire image
img, _, err := image.Decode(file)
if err != nil {
return "", err
}
pHash, err := goimagehash.PerceptionHash(img)
if err != nil {
return "", err
} else {
return fmt.Sprintf("%016x", pHash.GetHash()), nil
}
}