Skip to content
Open
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
24 changes: 19 additions & 5 deletions internal/importer/archive/rar/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,24 @@ func (rh *rarProcessor) AnalyzeRarContentFromNzb(ctx context.Context, rarFiles [
}

// Normalize RAR part filenames (e.g., part010 -> part10) for consistent processing
// Check if ALL files have no extension - if so, we'll add .partXX.rar extensions
allFilesNoExt := true
// Check if any files have no extension or if there are duplicate filenames - if so, we'll treat it as allFilesNoExt and add extensions
// seenNames detects obfuscated archives where every volume has the same name (or no extension).
// We break on the first duplicate found — that's sufficient to identify the obfuscated case.
allFilesNoExt := false
seenNames := make(map[string]struct{})
for _, file := range rarFiles {
if archive.HasExtension(file.Filename) {
allFilesNoExt = false
if !archive.HasExtension(file.Filename) {
allFilesNoExt = true
break
}
// Duplicate filename detected: this is an obfuscated multi-volume set where
// all parts share the same name. Treat as no-extension so we assign synthetic
// partXX.rar names to distinguish each volume.
if _, exists := seenNames[file.Filename]; exists {
allFilesNoExt = true
break
}
seenNames[file.Filename] = struct{}{}
}

// Get base filename from first file if all files have no extension
Expand All @@ -86,9 +97,12 @@ func (rh *rarProcessor) AnalyzeRarContentFromNzb(ctx context.Context, rarFiles [
slices.SortFunc(rarFiles, func(a, b parser.ParsedFile) int {
return strings.Compare(a.Filename, b.Filename)
})
// Use the first file's name as the base for all parts
// Use the first file's name as the base for all parts (stripping any added extension)
if len(rarFiles) > 0 {
baseFilename = rarFiles[0].Filename
if ext := filepath.Ext(baseFilename); ext != "" {
baseFilename = strings.TrimSuffix(baseFilename, ext)
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/importer/archive/rar/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ func normalizeRarPartFilename(filename string, index int, allFilesNoExt bool, to
// If all files have no extension, use baseFilename with .rXX extension
// This ensures all parts of the same archive have the same base filename
// Using RAR multi-volume convention: .r00, .r01, .r02, etc. (0-based)
if allFilesNoExt && !archive.HasExtension(filename) && baseFilename != "" {
if allFilesNoExt && (!archive.HasExtension(filename) || strings.HasSuffix(filename, ".rar")) && baseFilename != "" {
// Calculate padding width based on total number of files (0-based, so totalFiles-1)
width := len(strconv.Itoa(totalFiles - 1))
// Format with zero-padding (index is already 0-based from OriginalIndex)
Expand Down
41 changes: 41 additions & 0 deletions internal/importer/archive/rar/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,44 @@ func oldRollSetSkip(base string, sCount int, skip string) []string {
}
return out
}

func TestNormalizeRarPartFilenameMix(t *testing.T) {
tests := []struct {
name string
filename string
index int
allNoExt bool
totalFiles int
baseFilename string
want string
}{
{
name: "first file with .rar extension gets normalized to .r00",
filename: "my_show.rar",
index: 0,
allNoExt: true,
totalFiles: 46,
baseFilename: "my_show",
want: "my_show.r00",
},
{
name: "continuation file with no extension gets normalized to .r01",
filename: "my_show",
index: 1,
allNoExt: true,
totalFiles: 46,
baseFilename: "my_show",
want: "my_show.r01",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := normalizeRarPartFilename(tt.filename, tt.index, tt.allNoExt, tt.totalFiles, tt.baseFilename)
if got != tt.want {
t.Errorf("normalizeRarPartFilename(%q) = %q; want %q", tt.filename, got, tt.want)
}
})
}
}

19 changes: 14 additions & 5 deletions internal/importer/archive/sevenzip/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -533,13 +533,19 @@ func extractBaseFilenameSevenZip(filename string) string {

// renameSevenZipFilesAndSort renames all 7z files to have the same base name and sorts them
func renameSevenZipFilesAndSort(sevenZipFiles []parser.ParsedFile) []parser.ParsedFile {
// Check if ALL files have no extension - if so, we'll add .XXX extensions
allFilesNoExt := true
// Check if any files have no extension or if there are duplicate filenames - if so, we'll treat it as allFilesNoExt and add extensions
allFilesNoExt := false
seenNames := make(map[string]struct{})
for _, file := range sevenZipFiles {
if archive.HasExtension(file.Filename) {
allFilesNoExt = false
if !archive.HasExtension(file.Filename) {
allFilesNoExt = true
break
}
if _, exists := seenNames[file.Filename]; exists {
allFilesNoExt = true
break
}
seenNames[file.Filename] = struct{}{}
}

// Get base filename from first file if all files have no extension
Expand All @@ -549,9 +555,12 @@ func renameSevenZipFilesAndSort(sevenZipFiles []parser.ParsedFile) []parser.Pars
sort.Slice(sevenZipFiles, func(i, j int) bool {
return sevenZipFiles[i].Filename < sevenZipFiles[j].Filename
})
// Use the first file's name as the base for all parts
// Use the first file's name as the base for all parts (stripping any added extension)
if len(sevenZipFiles) > 0 {
baseFilename = sevenZipFiles[0].Filename
if ext := filepath.Ext(baseFilename); ext != "" {
baseFilename = strings.TrimSuffix(baseFilename, ext)
}
}
} else {
// Sort files by part number BEFORE extracting base filename
Expand Down
3 changes: 2 additions & 1 deletion internal/importer/archive/sevenzip/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"regexp"
"strconv"
"strings"

"github.com/javi11/altmount/internal/importer/archive"
)
Expand All @@ -30,7 +31,7 @@ func normalize7zPartFilename(filename string, index int, allFilesNoExt bool, tot
// If all files have no extension, use baseFilename with .XXX extension
// This ensures all parts of the same archive have the same base filename
// Using 7zip multi-volume convention: .001, .002, .003, etc. (1-based)
if allFilesNoExt && !archive.HasExtension(filename) && baseFilename != "" {
if allFilesNoExt && (!archive.HasExtension(filename) || strings.HasSuffix(filename, ".7z")) && baseFilename != "" {
// Calculate padding width based on total number of files (1-based, so totalFiles)
width := len(strconv.Itoa(totalFiles))
// Format with zero-padding (convert 0-based index to 1-based: index+1)
Expand Down
Loading