Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 2 additions & 16 deletions cmd/cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
package cmd

import (
"log/slog"
"math"

"github.com/ostafen/digler/internal/disk"
"github.com/ostafen/digler/internal/logger"
"github.com/ostafen/digler/internal/scan"
"github.com/ostafen/digler/pkg/util/format"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -78,7 +78,7 @@ func parseOptions(cmd *cobra.Command) scan.Options {
MaxFileSize: maxFileSize,
DisableLog: disableLog,
FileExt: fileExt,
LogLevel: slogLevel(logLevel),
LogLevel: logger.ParseLevel(logLevel),
}
}

Expand All @@ -91,17 +91,3 @@ func getBytes(cmd *cobra.Command, name string) uint64 {
}
return v
}

func slogLevel(level string) slog.Level {
switch level {
case "INFO":
return slog.LevelInfo
case "DEBUG":
return slog.LevelDebug
case "WARN":
return slog.LevelWarn
case "ERROR":
return slog.LevelError
}
return slog.LevelInfo
}
7 changes: 4 additions & 3 deletions internal/format/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import (
"bytes"
"fmt"
"io"
"log/slog"

"github.com/ostafen/digler/internal/logger"
"github.com/ostafen/digler/pkg/pbar"
"github.com/ostafen/digler/pkg/reader"
)
Expand All @@ -35,7 +35,7 @@ type Scanner struct {
buf []byte

r *FileRegistry
logger *slog.Logger
logger *logger.Logger
bufReader *reader.BufferedReadSeeker
}

Expand All @@ -47,7 +47,7 @@ type FileInfo struct {
}

func NewScanner(
logger *slog.Logger,
logger *logger.Logger,
r *FileRegistry,
bufferSize,
blockSize int,
Expand All @@ -68,6 +68,7 @@ func (sc *Scanner) Scan(r io.ReaderAt, size uint64) func(yield func(FileInfo) bo
stop := false

pb := pbar.NewProgressBarState(int64(size))
defer pb.Finish()

filesFound := 0

Expand Down
104 changes: 104 additions & 0 deletions internal/logger/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) 2025 Stefano Scafiti
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package logger

import (
"fmt"
"io"
"sync"
)

// Level type for log levels
type Level int

const (
DebugLevel Level = iota
InfoLevel
WarnLevel
ErrorLevel
)

func ParseLevel(level string) Level {
switch level {
case "INFO":
return InfoLevel
case "DEBUG":
return DebugLevel
case "WARN":
return WarnLevel
case "ERROR":
return ErrorLevel
}
return InfoLevel
}

func (l Level) String() string {
switch l {
case DebugLevel:
return "DEBUG"
case InfoLevel:
return "INFO"
case WarnLevel:
return "WARN"
case ErrorLevel:
return "ERROR"
default:
return "UNKNOWN"
}
}

// Logger defines the logging structure
type Logger struct {
mu sync.Mutex
out io.Writer
level Level
}

// New creates a new logger writing to a writer with minimum log level
func New(w io.Writer, level Level) *Logger {
return &Logger{
out: w,
level: level,
}
}

// log is the internal formatter
func (l *Logger) log(level Level, msg string) {
if level < l.level {
return
}

l.mu.Lock()
defer l.mu.Unlock()

fmt.Fprintf(l.out, "[%s] %s\n", level.String(), msg)
}

// --- Logging Methods ---

func (l *Logger) Debug(msg string) { l.log(DebugLevel, msg) }
func (l *Logger) Info(msg string) { l.log(InfoLevel, msg) }
func (l *Logger) Warn(msg string) { l.log(WarnLevel, msg) }
func (l *Logger) Error(msg string) { l.log(ErrorLevel, msg) }

func (l *Logger) Debugf(format string, args ...any) { l.log(DebugLevel, fmt.Sprintf(format, args...)) }
func (l *Logger) Infof(format string, args ...any) { l.log(InfoLevel, fmt.Sprintf(format, args...)) }
func (l *Logger) Warnf(format string, args ...any) { l.log(WarnLevel, fmt.Sprintf(format, args...)) }
func (l *Logger) Errorf(format string, args ...any) { l.log(ErrorLevel, fmt.Sprintf(format, args...)) }
86 changes: 32 additions & 54 deletions internal/scan/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,9 @@
package scan

import (
"bufio"
"encoding/binary"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"strings"
Expand All @@ -34,8 +32,10 @@ import (
"github.com/ostafen/digler/internal/env"
"github.com/ostafen/digler/internal/format"
"github.com/ostafen/digler/internal/fs"
"github.com/ostafen/digler/internal/logger"
"github.com/ostafen/digler/pkg/dfxml"
fmtutil "github.com/ostafen/digler/pkg/util/format"
ioutil "github.com/ostafen/digler/pkg/util/io"
)

type Options struct {
Expand All @@ -47,7 +47,7 @@ type Options struct {
MaxFileSize uint64
DisableLog bool
FileExt []string
LogLevel slog.Level
LogLevel logger.Level
}

func Scan(filePath string, opts Options) error {
Expand Down Expand Up @@ -140,20 +140,28 @@ func ScanPartition(p *disk.Partition, filePath string, opts Options) error {
fileExts[i] = headers[i].Ext
}

fmt.Println("[INFO] Starting scanning operation...")
fmt.Printf("[INFO] Source: \t%s\n", absPath(filePath))
fmt.Printf("[INFO] File Types: \t%s\n", strings.Join(fileExts, ","))
logger, logFile, err := setupLogger(logFilePath, opts.LogLevel)
if err != nil {
return err
}
if logFile != nil {
defer logFile.Close()
}

logger.Info("Starting scanning operation...")
logger.Infof("Source: \t%s", absPath(filePath))
logger.Infof("File Types: \t%s", strings.Join(fileExts, ","))

if opts.DumpDir != "" {
fmt.Printf("[INFO] Destination: \t%s\n", absPath(opts.DumpDir))
logger.Infof("Destination: \t%s", absPath(opts.DumpDir))
}

outLog := "disabled"
if !opts.DisableLog {
outLog = logFilePath
}
fmt.Printf("[INFO] Output Log: \t%s\n", outLog)
fmt.Printf("[INFO] Scanning for %d signatures...\n", registry.Signatures())
logger.Infof("Output Log: \t%s", outLog)
logger.Infof("Scanning for %d signatures...", registry.Signatures())

size := min(opts.MaxScanSize, p.Size)
r := io.NewSectionReader(f, int64(p.Offset), int64(size))
Expand All @@ -164,14 +172,6 @@ func ScanPartition(p *disk.Partition, filePath string, opts Options) error {
}
}

logger, logFile, err := setupLogger(logFilePath, opts.LogLevel)
if err != nil {
return err
}
if logFile != nil {
defer logFile.Close()
}

start := time.Now()
filesFound := 0
var totalDataSize uint64 = 0
Expand All @@ -189,7 +189,8 @@ func ScanPartition(p *disk.Partition, filePath string, opts Options) error {

if opts.DumpDir != "" {
fileReader := io.NewSectionReader(r, int64(finfo.Offset), int64(finfo.Size))
err := dumpFile(opts.DumpDir, finfo.Name, fileReader)

err := ioutil.CopyFile(filepath.Join(opts.DumpDir, finfo.Name), fileReader)
if err != nil {
return err
}
Expand All @@ -207,39 +208,22 @@ func ScanPartition(p *disk.Partition, filePath string, opts Options) error {
},
})
if err != nil {
logger.Error("unable to write index entry", "err", err)
logger.Errorf("unable to write index entry: %s", err)
}
}

fmt.Println()

fmt.Printf("[INFO] Scan completed!\n")
fmt.Printf("[INFO] Files found: \t%d\n", filesFound)
fmt.Printf("[INFO] Total data: \t%s\n", fmtutil.FormatBytes(int64(size)))
fmt.Printf("[INFO] Duration: \t%s\n", FormatDurationHMS(time.Since(start)))
fmt.Printf("[INFO] Report saved to: \t%s\n", absPath(reportFileName))
logger.Infof("Scan completed!")
logger.Infof("Files found: \t%d", filesFound)
logger.Infof("Total data: \t%s", fmtutil.FormatBytes(int64(size)))
logger.Infof("Duration: \t%s", FormatDurationHMS(time.Since(start)))
logger.Infof("Report saved to: \t%s", absPath(reportFileName))

if !opts.DisableLog {
fmt.Printf("[INFO] Detailed scan log: \t%s\n", logFilePath)
logger.Infof("Detailed scan log: \t%s", logFilePath)
}
return nil
}

func dumpFile(dumpDir string, fileName string, r io.Reader) error {
f, err := os.Create(filepath.Join(dumpDir, fileName))
if err != nil {
return fmt.Errorf("failed to create file %q: %w", fileName, err)
}
defer f.Close()

w := bufio.NewWriterSize(f, 1024*1024) // 1MB buffer

if _, err := io.Copy(w, r); err != nil {
return err
}
return w.Flush()
}

func DiscoverPartitions(path string) ([]disk.Partition, error) {
imgFile, err := fs.Open(path)
if err != nil {
Expand Down Expand Up @@ -374,13 +358,11 @@ func FormatDurationHMS(d time.Duration) string {
// - minLevel: The minimum log level to write.
// It returns the logger instance and the *os.File, which will be nil if logging to file is disabled.
// The returned *os.File (if not nil) should be closed by the caller.
func setupLogger(logFilePath string, minLevel slog.Level) (*slog.Logger, *os.File, error) {
var writer io.Writer
func setupLogger(logFilePath string, minLevel logger.Level) (*logger.Logger, *os.File, error) {
var w io.Writer = os.Stdout
var file *os.File

if logFilePath == "" {
writer = io.Discard
} else {
if logFilePath != "" {
logDir := filepath.Dir(logFilePath)
if err := os.MkdirAll(logDir, 0755); err != nil {
return nil, nil, fmt.Errorf("failed to create log directory %q: %w", logDir, err)
Expand All @@ -390,15 +372,11 @@ func setupLogger(logFilePath string, minLevel slog.Level) (*slog.Logger, *os.Fil
if err != nil {
return nil, nil, fmt.Errorf("failed to open log file %q: %w", logFilePath, err)
}
writer = f

w = io.MultiWriter(os.Stdout, f)
file = f
}

handler := slog.NewTextHandler(writer, &slog.HandlerOptions{
Level: minLevel,
AddSource: true,
})

logger := slog.New(handler)
logger := logger.New(w, logger.Level(minLevel))
return logger, file, nil
}
43 changes: 43 additions & 0 deletions pkg/util/io/io.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) 2025 Stefano Scafiti
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package io

import (
"bufio"
"fmt"
"io"
"os"
)

// CopyFile copies data from the provided reader to the file at filePath.
// It creates or truncates the file and writes using a 32KB buffer.
func CopyFile(filePath string, r io.Reader) error {
f, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("failed to create file %q: %w", filePath, err)
}
defer f.Close()

w := bufio.NewWriterSize(f, 32*1024)
if _, err := io.Copy(w, r); err != nil {
return err
}
return w.Flush()
}
Loading