581 lines
21 KiB
Go
581 lines
21 KiB
Go
package handlers
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
|
|
"github.com/gabriel-vasile/mimetype"
|
|
"github.com/mholt/archives"
|
|
|
|
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/iobuf"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
|
|
)
|
|
|
|
// fileReader is a custom reader that wraps an io.Reader and provides additional functionality for identifying
|
|
// and handling different file types. It abstracts away the complexity of detecting file formats, MIME types,
|
|
// and archive types, allowing for a more modular and extensible file handling process.
|
|
//
|
|
// fileReader leverages the archiver and mimetype packages for file type identification and provides information
|
|
// about the detected file format, MIME type, and whether the file is an archive. This information can be
|
|
// used by FileHandler implementations to make decisions on how to process the file.
|
|
//
|
|
// The IsGenericArchive field indicates whether the file represents an archive format that is supported by the
|
|
// archiver library. This allows FileHandler implementations to determine if the file can be processed using
|
|
// the default archive handling capabilities provided by the archiver package.
|
|
//
|
|
// By encapsulating the file type detection logic, fileReader simplifies the implementation of FileHandler and
|
|
// promotes a more cohesive and maintainable codebase. It also embeds a BufferedFileReader to provide efficient
|
|
// random access to the file content.
|
|
type fileReader struct {
|
|
format archives.Format
|
|
mime *mimetype.MIME
|
|
isGenericArchive bool
|
|
|
|
*iobuf.BufferedReadSeeker
|
|
}
|
|
|
|
var (
|
|
ErrEmptyReader = errors.New("reader is empty")
|
|
|
|
// ErrProcessingFatal indicates a severe error that requires stopping the file processing.
|
|
ErrProcessingFatal = errors.New("fatal error processing file")
|
|
|
|
// ErrProcessingWarning indicates a recoverable error that can be logged,
|
|
// allowing processing to continue.
|
|
ErrProcessingWarning = errors.New("error processing file")
|
|
)
|
|
|
|
type readerConfig struct{ fileExtension string }
|
|
|
|
type readerOption func(*readerConfig)
|
|
|
|
func withFileExtension(ext string) readerOption {
|
|
return func(c *readerConfig) { c.fileExtension = ext }
|
|
}
|
|
|
|
// mimeTypeReader wraps an io.Reader with MIME type information.
|
|
// This type is used to pass content through the processing pipeline
|
|
// while carrying its detected MIME type, avoiding redundant type detection.
|
|
type mimeTypeReader struct {
|
|
mimeExt string
|
|
mimeName mimeType
|
|
io.Reader
|
|
}
|
|
|
|
// newMimeTypeReaderFromFileReader creates a new mimeTypeReader from a fileReader.
|
|
func newMimeTypeReaderFromFileReader(r fileReader) mimeTypeReader {
|
|
return mimeTypeReader{
|
|
mimeExt: r.mime.Extension(),
|
|
mimeName: mimeType(r.mime.String()),
|
|
Reader: r.BufferedReadSeeker,
|
|
}
|
|
}
|
|
|
|
// newMimeTypeReader creates a new mimeTypeReader from an io.Reader.
|
|
// It uses a bufio.Reader to perform MIME type detection on the input reader
|
|
// without consuming it, by peeking into the first 3072 bytes of the input.
|
|
// This encapsulates both the original reader and the detected MIME type information.
|
|
// This function is particularly useful for specialized archive handlers
|
|
// that need to pass extracted content to the default handler without modifying the original reader.
|
|
func newMimeTypeReader(r io.Reader) (mimeTypeReader, error) {
|
|
const defaultMinBufferSize = 3072
|
|
bufReader := bufio.NewReaderSize(r, defaultMinBufferSize)
|
|
// A buffer of 512 bytes is used since many file formats store their magic numbers within the first 512 bytes.
|
|
// If fewer bytes are read, MIME type detection may still succeed.
|
|
buffer, err := bufReader.Peek(defaultMinBufferSize)
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
return mimeTypeReader{}, fmt.Errorf("unable to read file for MIME type detection: %w", err)
|
|
}
|
|
|
|
mime := mimetype.Detect(buffer)
|
|
|
|
return mimeTypeReader{mimeExt: mime.Extension(), mimeName: mimeType(mime.String()), Reader: bufReader}, nil
|
|
}
|
|
|
|
// newFileReader creates a fileReader from an io.Reader, optionally using BufferedFileWriter for certain formats.
|
|
// The caller is responsible for closing the reader when it is no longer needed.
|
|
func newFileReader(ctx context.Context, r io.Reader, options ...readerOption) (fReader fileReader, err error) {
|
|
var cfg readerConfig
|
|
|
|
for _, opt := range options {
|
|
opt(&cfg)
|
|
}
|
|
// To detect the MIME type of the input data, we need a reader that supports seeking.
|
|
// This allows us to read the data multiple times if necessary without losing the original position.
|
|
// We use a BufferedReaderSeeker to wrap the original reader, enabling this functionality.
|
|
fReader.BufferedReadSeeker = iobuf.NewBufferedReaderSeeker(r)
|
|
|
|
// If an error occurs during MIME type detection, it is important we close the BufferedReaderSeeker
|
|
// to release any resources it holds (checked out buffers or temp file).
|
|
defer func() {
|
|
if err != nil {
|
|
if closeErr := fReader.Close(); closeErr != nil {
|
|
err = fmt.Errorf("%w; error closing reader: %w", err, closeErr)
|
|
}
|
|
}
|
|
}()
|
|
|
|
var mime *mimetype.MIME
|
|
mime, err = mimetype.DetectReader(fReader)
|
|
if err != nil {
|
|
return fReader, fmt.Errorf("unable to detect MIME type: %w", err)
|
|
}
|
|
fReader.mime = mime
|
|
|
|
// Reset the reader to the beginning because DetectReader consumes the reader.
|
|
if _, err = fReader.Seek(0, io.SeekStart); err != nil {
|
|
return fReader, fmt.Errorf("error resetting reader after MIME detection: %w", err)
|
|
}
|
|
|
|
// Check for APK files
|
|
if shouldHandleAsAPK(cfg, fReader) {
|
|
isAPK, err := isAPKFile(&fReader)
|
|
if err != nil {
|
|
return fReader, fmt.Errorf("error checking for APK: %w", err)
|
|
}
|
|
if isAPK {
|
|
return handleAPKFile(&fReader)
|
|
}
|
|
}
|
|
|
|
// If a MIME type is known to not be an archive type, we might as well return here rather than
|
|
// paying the I/O penalty of an archiver.Identify() call that won't identify anything.
|
|
if _, ok := skipArchiverMimeTypes[mimeType(mime.String())]; ok {
|
|
return fReader, nil
|
|
}
|
|
|
|
var format archives.Format
|
|
format, _, err = archives.Identify(ctx, "", fReader)
|
|
switch {
|
|
case err == nil:
|
|
fReader.isGenericArchive = true
|
|
fReader.format = format
|
|
|
|
case errors.Is(err, archives.NoMatch):
|
|
// Not an archive handled by archiver.
|
|
// Continue with the default reader.
|
|
default:
|
|
return fReader, fmt.Errorf("error identifying archive: %w", err)
|
|
}
|
|
|
|
// Reset the reader to the beginning again to allow the handler to read from the start.
|
|
// This is necessary because Identify consumes the reader.
|
|
if _, err = fReader.Seek(0, io.SeekStart); err != nil {
|
|
return fReader, fmt.Errorf("error resetting reader after archive identification: %w", err)
|
|
}
|
|
|
|
return fReader, nil
|
|
}
|
|
|
|
// DataOrErr represents a result that can either contain data or an error.
|
|
// The Data field holds the byte slice of data, and the Err field holds any error that occurred.
|
|
// This structure is used to handle asynchronous file processing where each chunk of data
|
|
// or potential error needs to be communicated back to the caller. It allows for
|
|
// efficient streaming of file contents while also providing a way to propagate errors
|
|
// that may occur during the file handling process.
|
|
type DataOrErr struct {
|
|
Data []byte
|
|
Err error
|
|
}
|
|
|
|
// FileHandler represents a handler for files.
|
|
// It has a single method, HandleFile, which takes a context and a fileReader as input,
|
|
// and returns a channel of byte slices and an error.
|
|
type FileHandler interface {
|
|
HandleFile(ctx logContext.Context, reader fileReader) chan DataOrErr
|
|
}
|
|
|
|
// fileHandlingConfig encapsulates configuration settings that control the behavior of file processing.
|
|
type fileHandlingConfig struct{ skipArchives bool }
|
|
|
|
// newFileHandlingConfig creates a default fileHandlingConfig with default settings.
|
|
// Optional functional parameters can customize the configuration.
|
|
func newFileHandlingConfig(options ...func(*fileHandlingConfig)) fileHandlingConfig {
|
|
config := fileHandlingConfig{}
|
|
for _, option := range options {
|
|
option(&config)
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
// WithSkipArchives sets the skipArchives field of the fileHandlingConfig.
|
|
// If skip is true, the FileHandler will skip archive files.
|
|
func WithSkipArchives(skip bool) func(*fileHandlingConfig) {
|
|
return func(c *fileHandlingConfig) { c.skipArchives = skip }
|
|
}
|
|
|
|
type handlerType string
|
|
|
|
const (
|
|
archiveHandlerType handlerType = "archive"
|
|
arHandlerType handlerType = "ar"
|
|
rpmHandlerType handlerType = "rpm"
|
|
apkHandlerType handlerType = "apk"
|
|
defaultHandlerType handlerType = "default"
|
|
apkExt = ".apk"
|
|
)
|
|
|
|
type mimeType string
|
|
|
|
const (
|
|
rpmMime mimeType = "application/x-rpm"
|
|
cpioMime mimeType = "application/cpio"
|
|
unixArMime mimeType = "application/x-unix-archive"
|
|
arMime mimeType = "application/x-archive"
|
|
debMime mimeType = "application/vnd.debian.binary-package"
|
|
textMime mimeType = "text/plain; charset=utf-8"
|
|
xmlMime mimeType = "text/xml"
|
|
jsonMime mimeType = "application/json"
|
|
csvMime mimeType = "text/csv"
|
|
tsvMime mimeType = "text/tab-separated-values"
|
|
geoJSONMine mimeType = "application/vnd.geo+json"
|
|
ndjsonMime mimeType = "application/x-ndjson"
|
|
htmlMime mimeType = "text/html"
|
|
phpTextMime mimeType = "text/x-php"
|
|
rtfTextMime mimeType = "text/rtf"
|
|
jsAppMime mimeType = "application/javascript"
|
|
jsTextMime mimeType = "text/javascript"
|
|
jsMime mimeType = "application/x-javascript"
|
|
srtMime mimeType = "application/x-subrip"
|
|
srtXMime mimeType = "application/x-srt"
|
|
srtTextMime mimeType = "text/x-srt"
|
|
vttMime mimeType = "text/vtt"
|
|
luaMime mimeType = "text/x-lua"
|
|
perlMime mimeType = "text/x-perl"
|
|
pythonMime mimeType = "text/x-python"
|
|
pyAppMime mimeType = "application/x-python"
|
|
pyScriptMime mimeType = "application/x-script.python"
|
|
tclTextMime mimeType = "text/x-tcl"
|
|
tclMime mimeType = "application/x-tcl"
|
|
apkMime mimeType = "application/vnd.android.package-archive"
|
|
zipMime mimeType = "application/zip"
|
|
jarMime mimeType = "application/java-archive"
|
|
)
|
|
|
|
// skipArchiverMimeTypes is a set of MIME types that should bypass archiver library processing because they are either
|
|
// text-based or archives not supported by the library.
|
|
var skipArchiverMimeTypes = map[mimeType]struct{}{
|
|
arMime: {},
|
|
unixArMime: {},
|
|
debMime: {},
|
|
rpmMime: {},
|
|
cpioMime: {},
|
|
textMime: {},
|
|
xmlMime: {},
|
|
jsonMime: {},
|
|
csvMime: {},
|
|
tsvMime: {},
|
|
geoJSONMine: {},
|
|
ndjsonMime: {},
|
|
htmlMime: {},
|
|
phpTextMime: {},
|
|
rtfTextMime: {},
|
|
jsAppMime: {},
|
|
jsTextMime: {},
|
|
jsMime: {},
|
|
srtMime: {},
|
|
srtXMime: {},
|
|
srtTextMime: {},
|
|
vttMime: {},
|
|
luaMime: {},
|
|
perlMime: {},
|
|
pythonMime: {},
|
|
pyAppMime: {},
|
|
pyScriptMime: {},
|
|
tclTextMime: {},
|
|
tclMime: {},
|
|
apkMime: {},
|
|
}
|
|
|
|
// selectHandler dynamically selects and configures a FileHandler based on the provided |mimetype| type and archive flag.
|
|
// The fileReader contains information about the MIME type and whether the file is an archive.
|
|
// This method uses specialized handlers for specific file types:
|
|
// - arHandler is used for Unix archives and Debian packages ('arMime', 'unixArMime', and 'debMime').
|
|
// - rpmHandler is used for RPM and CPIO archives ('rpmMime' and 'cpioMime').
|
|
// - apkHandler is used for APK archives ('apkMime').
|
|
// - archiveHandler is used for common archive formats supported by the archiver library (.zip, .tar, .gz, etc.).
|
|
// - defaultHandler is used for non-archive files.
|
|
// The selected handler is then returned, ready to handle the file according to its specific format and requirements.
|
|
func selectHandler(mimeT mimeType, isGenericArchive bool) FileHandler {
|
|
switch mimeT {
|
|
case arMime, unixArMime, debMime:
|
|
return newARHandler()
|
|
case rpmMime, cpioMime:
|
|
return newRPMHandler()
|
|
case apkMime:
|
|
return newAPKHandler()
|
|
default:
|
|
if isGenericArchive {
|
|
return newArchiveHandler()
|
|
}
|
|
return newDefaultHandler(defaultHandlerType)
|
|
}
|
|
}
|
|
|
|
// HandleFile orchestrates the complete file handling process for a given file.
|
|
// It determines the MIME type of the file,
|
|
// selects the appropriate handler based on this type, and processes the file.
|
|
// This function initializes the handling process and delegates to the specific
|
|
// handler to manage file extraction or processing.
|
|
//
|
|
// The function will return nil (success) in the following cases:
|
|
// - If the reader is empty (ErrEmptyReader)
|
|
// - If skipArchives option is true and the file is detected as an archive
|
|
// - If all chunks are processed successfully without critical errors
|
|
//
|
|
// The function will return an error in the following cases:
|
|
// - If the reader is nil
|
|
// - If there's an error creating the file reader
|
|
// - If there's an error closing the reader
|
|
// - If a critical error occurs during chunk processing (context cancellation, deadline exceeded, or ErrProcessingFatal)
|
|
// - If there's an error reporting a chunk
|
|
//
|
|
// Non-critical errors during chunk processing are logged
|
|
// but do not cause the function to return an error.
|
|
func HandleFile(
|
|
ctx logContext.Context,
|
|
reader io.Reader,
|
|
chunkSkel *sources.Chunk,
|
|
reporter sources.ChunkReporter,
|
|
options ...func(*fileHandlingConfig),
|
|
) error {
|
|
if reader == nil {
|
|
return errors.New("reader is nil")
|
|
}
|
|
|
|
readerOption := withFileExtension(getFileExtension(chunkSkel))
|
|
rdr, err := newFileReader(ctx, reader, readerOption)
|
|
if err != nil {
|
|
if errors.Is(err, ErrEmptyReader) {
|
|
ctx.Logger().V(5).Info("empty reader, skipping file")
|
|
return nil
|
|
}
|
|
return fmt.Errorf("unable to HandleFile, error creating file reader: %w", err)
|
|
}
|
|
defer func() {
|
|
// Ensure all data is read to prevent broken pipe.
|
|
if closeErr := rdr.Close(); closeErr != nil {
|
|
if err != nil {
|
|
err = errors.Join(err, closeErr)
|
|
} else {
|
|
err = fmt.Errorf("error closing reader: %w", closeErr)
|
|
}
|
|
}
|
|
}()
|
|
|
|
ctx = logContext.WithValues(ctx, "mime", rdr.mime.String())
|
|
|
|
mimeT := mimeType(rdr.mime.String())
|
|
config := newFileHandlingConfig(options...)
|
|
if config.skipArchives && rdr.isGenericArchive {
|
|
ctx.Logger().V(5).Info("skipping archive file", "mime", mimeT)
|
|
return nil
|
|
}
|
|
|
|
processingCtx, cancel := logContext.WithTimeout(ctx, maxTimeout)
|
|
defer cancel()
|
|
|
|
handler := selectHandler(mimeT, rdr.isGenericArchive)
|
|
dataOrErrChan := handler.HandleFile(processingCtx, rdr) // Delegate to the specific handler to process the file.
|
|
|
|
return handleChunksWithError(processingCtx, dataOrErrChan, chunkSkel, reporter)
|
|
}
|
|
|
|
// handleChunksWithError processes data and errors received from the dataErrChan channel.
|
|
// For each DataOrErr received:
|
|
// - If it contains data, the function creates a chunk based on chunkSkel and reports it through the reporter.
|
|
// - If it contains an error, the function handles it based on severity:
|
|
// - Fatal errors (context cancellation, deadline exceeded, ErrProcessingFatal) cause immediate termination
|
|
// - Non-fatal errors (ErrProcessingWarning and others) are logged and processing continues
|
|
//
|
|
// The function also listens for context cancellation to gracefully terminate processing if the context is done.
|
|
// It returns nil upon successful processing of all data, or the first encountered fatal error.
|
|
func handleChunksWithError(
|
|
ctx logContext.Context,
|
|
dataErrChan <-chan DataOrErr,
|
|
chunkSkel *sources.Chunk,
|
|
reporter sources.ChunkReporter,
|
|
) error {
|
|
for {
|
|
select {
|
|
case dataOrErr, ok := <-dataErrChan:
|
|
if !ok {
|
|
// Channel closed, processing complete.
|
|
ctx.Logger().V(5).Info("dataErrChan closed, all chunks processed")
|
|
return nil
|
|
}
|
|
if dataOrErr.Err != nil {
|
|
if isFatal(dataOrErr.Err) {
|
|
return dataOrErr.Err
|
|
}
|
|
ctx.Logger().Error(dataOrErr.Err, "non-critical error processing chunk")
|
|
continue
|
|
}
|
|
if len(dataOrErr.Data) > 0 {
|
|
chunk := *chunkSkel
|
|
chunk.Data = dataOrErr.Data
|
|
if err := reporter.ChunkOk(ctx, chunk); err != nil {
|
|
return fmt.Errorf("error reporting chunk: %w", err)
|
|
}
|
|
}
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
// isFatal determines whether the given error is a fatal error that should
|
|
// terminate processing the current file, or a non-critical error that can be logged and ignored.
|
|
// "Fatal" errors include context cancellation, deadline exceeded, and the
|
|
// ErrProcessingFatal error. Non-fatal errors include the ErrProcessingWarning
|
|
// error as well as any other error that is not one of the fatal errors.
|
|
func isFatal(err error) bool {
|
|
switch {
|
|
case errors.Is(err, context.Canceled) ||
|
|
errors.Is(err, context.DeadlineExceeded) ||
|
|
errors.Is(err, ErrProcessingFatal):
|
|
return true
|
|
case errors.Is(err, ErrProcessingWarning):
|
|
return false
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// getFileExtension extracts the file extension from the chunk's SourceMetadata.
|
|
// It considers all sources defined in the MetaData message.
|
|
// Note: Probably should add this as a method to the source_metadatapb object.
|
|
// then it'd just be chunkSkel.SourceMetadata.GetFileExtension()
|
|
func getFileExtension(chunkSkel *sources.Chunk) string {
|
|
if chunkSkel == nil || chunkSkel.SourceMetadata == nil {
|
|
return ""
|
|
}
|
|
|
|
var fileName string
|
|
|
|
// Inspect the SourceMetadata to determine the source type
|
|
switch metadata := chunkSkel.SourceMetadata.Data.(type) {
|
|
case *source_metadatapb.MetaData_Artifactory:
|
|
fileName = metadata.Artifactory.Path
|
|
case *source_metadatapb.MetaData_Azure:
|
|
fileName = metadata.Azure.File
|
|
case *source_metadatapb.MetaData_AzureRepos:
|
|
fileName = metadata.AzureRepos.File
|
|
case *source_metadatapb.MetaData_Bitbucket:
|
|
fileName = metadata.Bitbucket.File
|
|
case *source_metadatapb.MetaData_Buildkite:
|
|
fileName = metadata.Buildkite.Link
|
|
case *source_metadatapb.MetaData_Circleci:
|
|
fileName = metadata.Circleci.Link
|
|
case *source_metadatapb.MetaData_Confluence:
|
|
fileName = metadata.Confluence.File
|
|
case *source_metadatapb.MetaData_Docker:
|
|
fileName = metadata.Docker.File
|
|
case *source_metadatapb.MetaData_Ecr:
|
|
fileName = metadata.Ecr.File
|
|
case *source_metadatapb.MetaData_Filesystem:
|
|
fileName = metadata.Filesystem.File
|
|
case *source_metadatapb.MetaData_Git:
|
|
fileName = metadata.Git.File
|
|
case *source_metadatapb.MetaData_Github:
|
|
fileName = metadata.Github.File
|
|
case *source_metadatapb.MetaData_Gitlab:
|
|
fileName = metadata.Gitlab.File
|
|
case *source_metadatapb.MetaData_Gcs:
|
|
fileName = metadata.Gcs.Filename
|
|
case *source_metadatapb.MetaData_GoogleDrive:
|
|
fileName = metadata.GoogleDrive.File
|
|
case *source_metadatapb.MetaData_Huggingface:
|
|
fileName = metadata.Huggingface.File
|
|
case *source_metadatapb.MetaData_Jira:
|
|
fileName = metadata.Jira.Link
|
|
case *source_metadatapb.MetaData_Jenkins:
|
|
fileName = metadata.Jenkins.Link
|
|
case *source_metadatapb.MetaData_Npm:
|
|
fileName = metadata.Npm.File
|
|
case *source_metadatapb.MetaData_Pypi:
|
|
fileName = metadata.Pypi.File
|
|
case *source_metadatapb.MetaData_S3:
|
|
fileName = metadata.S3.File
|
|
case *source_metadatapb.MetaData_Slack:
|
|
fileName = metadata.Slack.File
|
|
case *source_metadatapb.MetaData_Sharepoint:
|
|
fileName = metadata.Sharepoint.Link
|
|
case *source_metadatapb.MetaData_Gerrit:
|
|
fileName = metadata.Gerrit.File
|
|
case *source_metadatapb.MetaData_Test:
|
|
fileName = metadata.Test.File
|
|
case *source_metadatapb.MetaData_Teams:
|
|
fileName = metadata.Teams.File
|
|
case *source_metadatapb.MetaData_TravisCI:
|
|
fileName = metadata.TravisCI.Link
|
|
// Add other sources if they have a file or equivalent field
|
|
// Skipping Syslog, Forager, Postman, Vector, Webhook and Elasticsearch
|
|
default:
|
|
return ""
|
|
}
|
|
|
|
// Use filepath.Ext to extract the file extension from the file name
|
|
ext := filepath.Ext(fileName)
|
|
return ext
|
|
}
|
|
|
|
// shouldHandleAsAPK checks if the file should be handled as an APK based on config and MIME type.
|
|
// Note: We can't extend the mimetype package with an APK detection function b/c it would require adjusting settings
|
|
// so that all files are fully read into a byte slice for detection (mimetype.SetLimit(0)), which would bloat memory.
|
|
// Instead we call the isAPKFile function in here after ensuring it's a zip/jar file and has an .apk extension.
|
|
func shouldHandleAsAPK(cfg readerConfig, fReader fileReader) bool {
|
|
return feature.EnableAPKHandler.Load() &&
|
|
cfg.fileExtension == apkExt &&
|
|
(fReader.mime.String() == string(zipMime) || fReader.mime.String() == string(jarMime))
|
|
}
|
|
|
|
func isAPKFile(r *fileReader) (bool, error) {
|
|
size, _ := r.Size()
|
|
zipReader, err := zip.NewReader(r, size)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error creating zip reader: %w", err)
|
|
}
|
|
|
|
hasManifest := false
|
|
hasClasses := false
|
|
|
|
for _, file := range zipReader.File {
|
|
switch file.Name {
|
|
case "AndroidManifest.xml":
|
|
hasManifest = true
|
|
case "classes.dex":
|
|
hasClasses = true
|
|
default:
|
|
// Skip other files.
|
|
}
|
|
if hasManifest && hasClasses {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// handleAPKFile configures the MIME type for an APK and resets the reader.
|
|
func handleAPKFile(fReader *fileReader) (fileReader, error) {
|
|
// Extend the MIME type to recognize APK files
|
|
mimetype.Lookup("application/zip").Extend(func(r []byte, l uint32) bool { return false }, string(apkMime), ".apk")
|
|
fReader.mime = mimetype.Lookup(string(apkMime))
|
|
|
|
// Reset reader for further handling
|
|
if _, err := fReader.Seek(0, io.SeekStart); err != nil {
|
|
return *fReader, fmt.Errorf("error resetting reader after APK detection: %w", err)
|
|
}
|
|
return *fReader, nil
|
|
}
|