diff --git a/README.md b/README.md index 7653168..b06c4ce 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![GitHub all releases](https://img.shields.io/github/downloads/rumanzo/bt2qbt/total) + # bt2qbt bt2qbt is cli tool for export from uTorrent\Bittorrent into qBittorrent (convert) @@ -5,7 +7,7 @@ bt2qbt is cli tool for export from uTorrent\Bittorrent into qBittorrent (convert > Actual version tested with uTorrent 3.5.5 (build 46206) and qBittorrent 4.4.2. It should work with older version utorrent and newer version of qBittorrent, but it isn't tested. > [!IMPORTANT] -> In most cases just enough run app. For windows users double click on downloaded exe file. But read notices and warnings below +> In most cases just enough run app. For Windows users double-click on downloaded exe file. But read notices and warnings below > - [bt2qbt](#bt2qbt) - [Feature](#user-content-feature) diff --git a/internal/transfer/fastresumeHandle.go b/internal/transfer/fastresumeHandle.go index 04a45c7..05f7a3d 100644 --- a/internal/transfer/fastresumeHandle.go +++ b/internal/transfer/fastresumeHandle.go @@ -15,11 +15,6 @@ func (transfer *TransferStructure) HandleStructures() { // if torrent name was renamed, add modified name transfer.HandleCaption() - if transfer.TorrentFile.Info.NameUTF8 != "" { - transfer.Fastresume.Name = helpers.HandleCesu8(transfer.TorrentFile.Info.NameUTF8) - } else { - transfer.Fastresume.Name = helpers.HandleCesu8(transfer.TorrentFile.Info.Name) - } transfer.Fastresume.ActiveTime = transfer.ResumeItem.Runtime transfer.Fastresume.AddedTime = transfer.ResumeItem.AddedOn transfer.Fastresume.CompletedTime = transfer.ResumeItem.CompletedOn @@ -46,6 +41,6 @@ func (transfer *TransferStructure) HandleStructures() { transfer.NumPieces = int64(len(transfer.TorrentFile.Info.Pieces)) / 20 transfer.HandleCompleted() // important handle priorities before handling pieces - transfer.HandleSavePaths() + transfer.HandleSavePaths() // and there we handle torrent name also transfer.HandlePieces() } diff --git a/internal/transfer/labels.go b/internal/transfer/labels.go index f9b0aad..1abc0c7 100644 --- a/internal/transfer/labels.go +++ b/internal/transfer/labels.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "github.com/rumanzo/bt2qbt/internal/options" - "io/ioutil" "os" ) @@ -14,7 +13,7 @@ func ProcessLabels(opts *options.Opts, newtags []string) error { // check if categories is new file. If it exists it must be unmarshaled. Default categories file contains only {} var categoriesIsNew bool - file, err := os.OpenFile(opts.Categories, os.O_RDWR, 0644) + _, err := os.Stat(opts.Categories) if errors.Is(err, os.ErrNotExist) { categoriesIsNew = true } else if err != nil { @@ -22,16 +21,11 @@ func ProcessLabels(opts *options.Opts, newtags []string) error { } if !categoriesIsNew { - dataRaw, err := ioutil.ReadAll(file) + dataRaw, err := os.ReadFile(opts.Categories) if err != nil { return errors.New(fmt.Sprintf("Unexpected error while read categories.json. Error:\n%v\n", err)) } - err = file.Close() - if err != nil { - return errors.New(fmt.Sprintf("Can't close categories.json. Error:\n%v\n", err)) - } - err = json.Unmarshal(dataRaw, &categories) if err != nil { return errors.New(fmt.Sprintf("Unexpected error while unmarshaling categories.json. Error:\n%v\n", err)) @@ -56,7 +50,7 @@ func ProcessLabels(opts *options.Opts, newtags []string) error { return errors.New(fmt.Sprintf("Can't marshal categories. Error:\n%v\n", err)) } - err = ioutil.WriteFile(opts.Categories, newCategories, 0644) + err = os.WriteFile(opts.Categories, newCategories, 0644) if err != nil { return errors.New(fmt.Sprintf("Can't write categories.json. Error:\n%v\n", err)) } diff --git a/internal/transfer/labels_test.go b/internal/transfer/labels_test.go index b6c080c..f3ac0de 100644 --- a/internal/transfer/labels_test.go +++ b/internal/transfer/labels_test.go @@ -2,13 +2,12 @@ package transfer import ( "github.com/rumanzo/bt2qbt/internal/options" - "io/ioutil" "os" "testing" ) func TestProcessLabelsExisting(t *testing.T) { - err := ioutil.WriteFile("../../test/categories_existing.json", []byte("{}"), 0755) + err := os.WriteFile("../../test/categories_existing.json", []byte("{}"), 0755) if err != nil { t.Fatalf("Can't write empty categories test file. Err: %v", err.Error()) } diff --git a/internal/transfer/resumeHandle.go b/internal/transfer/resumeHandle.go index a6c1fa8..30e727b 100644 --- a/internal/transfer/resumeHandle.go +++ b/internal/transfer/resumeHandle.go @@ -149,7 +149,7 @@ func HandleTorrentFilePath(transferStructure *TransferStructure, key string) { } } -// if we can find torrent file, we start check another locations from options search paths +// FindTorrentFile if we can find torrent file, we start check another locations from options search paths func FindTorrentFile(transferStructure *TransferStructure) error { if _, err := os.Stat(transferStructure.TorrentFilePath); os.IsNotExist(err) { for _, searchPath := range transferStructure.Opts.SearchPaths { diff --git a/internal/transfer/transfer.go b/internal/transfer/transfer.go index a578239..038189b 100644 --- a/internal/transfer/transfer.go +++ b/internal/transfer/transfer.go @@ -7,6 +7,7 @@ import ( "github.com/rumanzo/bt2qbt/internal/replace" "github.com/rumanzo/bt2qbt/pkg/fileHelpers" "github.com/rumanzo/bt2qbt/pkg/helpers" + "github.com/rumanzo/bt2qbt/pkg/normalization" "github.com/rumanzo/bt2qbt/pkg/qBittorrentStructures" "github.com/rumanzo/bt2qbt/pkg/torrentStructures" "github.com/rumanzo/bt2qbt/pkg/utorrentStructs" @@ -15,7 +16,6 @@ import ( "regexp" "strings" "time" - "unicode/utf8" ) //goland:noinspection GoNameStartsWithPackageName @@ -82,13 +82,13 @@ func (transfer *TransferStructure) HandleCaption() { } // HandleState transfer torrents state. -// if torrent has several files and it doesn't complete downloaded (priority), it will be stopped +// if torrent has several files, and it doesn't complete downloaded (priority), it will be stopped func (transfer *TransferStructure) HandleState() { if transfer.ResumeItem.Started == 0 { transfer.Fastresume.Paused = 1 transfer.Fastresume.AutoManaged = 0 } else { - if len(transfer.TorrentFile.GetFileList()) > 1 { + if !transfer.TorrentFile.IsSingle() { var parted bool for _, prio := range transfer.Fastresume.FilePriority { if prio == 0 { @@ -202,7 +202,7 @@ func (transfer *TransferStructure) HandlePieces() { if transfer.Fastresume.Unfinished != nil { transfer.FillWholePieces(0) } else { - if len(transfer.TorrentFile.GetFileList()) > 0 { + if !transfer.TorrentFile.IsSingle() { transfer.FillPiecesParted() } else { transfer.FillWholePieces(1) @@ -227,7 +227,8 @@ func (transfer *TransferStructure) FillPiecesParted() { } var fileOffsets []Offset bytesLength := int64(0) - for _, file := range transfer.TorrentFile.GetFileListWB() { // need to adapt for torrents v2 version + fileList, _ := transfer.TorrentFile.GetFileListWB() + for _, file := range fileList { fileFirstOffset := bytesLength + 1 bytesLength += file.Length fileLastOffset := bytesLength @@ -267,31 +268,20 @@ func (transfer *TransferStructure) HandleSavePaths() { // Original paths always ending with pathSeparator // SubFolder or NoSubfolder never have ending pathSeparator // qBtSavePath always has separator /, otherwise SavePath use os pathSeparator + if transfer.Magnet { transfer.Fastresume.QBtContentLayout = "Original" transfer.Fastresume.QbtSavePath = fileHelpers.Normalize(helpers.HandleCesu8(transfer.ResumeItem.Path), "/") } else { - var torrentName string - var torrentNameOriginal string - if transfer.TorrentFile.Info.NameUTF8 != "" { - torrentName = helpers.HandleCesu8(transfer.TorrentFile.Info.NameUTF8) - torrentNameOriginal = transfer.TorrentFile.Info.NameUTF8 - } else { - torrentName = helpers.HandleCesu8(transfer.TorrentFile.Info.Name) - torrentNameOriginal = transfer.TorrentFile.Info.Name - } - lastPathName := fileHelpers.Base(helpers.HandleCesu8(transfer.ResumeItem.Path)) + var nameNormalized bool + transfer.Fastresume.Name, nameNormalized = normalization.FullNormalize(transfer.TorrentFile.GetTorrentName()) - if len(transfer.TorrentFile.GetFileList()) > 0 { - var cesu8FilesExists bool - for _, filePath := range transfer.TorrentFile.GetFileList() { - cesuEncodedFilepath := helpers.HandleCesu8(filePath) - if utf8.RuneCountInString(filePath) != utf8.RuneCountInString(cesuEncodedFilepath) { - cesu8FilesExists = true - break - } - } - if lastPathName == torrentName && !cesu8FilesExists { + lastPathName := fileHelpers.Base(helpers.HandleCesu8(transfer.ResumeItem.Path)) + // if FileList contain only 1 file that means it is single file torrent + if !transfer.TorrentFile.IsSingle() { + fileList, filesNormalized := transfer.TorrentFile.GetFileList() + + if lastPathName == transfer.Fastresume.Name && !filesNormalized && !nameNormalized { transfer.Fastresume.QBtContentLayout = "Original" transfer.Fastresume.QbtSavePath = fileHelpers.CutLastPath(helpers.HandleCesu8(transfer.ResumeItem.Path), transfer.Opts.PathSeparator) if maxIndex := transfer.FindHighestIndexOfMappedFiles(); maxIndex >= 0 { @@ -309,7 +299,7 @@ func (transfer *TransferStructure) HandleSavePaths() { pathParts[num] = helpers.HandleCesu8(part.(string)) } // we have to append torrent name(from torrent file) at the top of path - transfer.Fastresume.MappedFiles[index] = fileHelpers.Join(append([]string{torrentName}, pathParts...), transfer.Opts.PathSeparator) + transfer.Fastresume.MappedFiles[index] = fileHelpers.Join(append([]string{transfer.Fastresume.Name}, pathParts...), transfer.Opts.PathSeparator) } } } @@ -319,10 +309,10 @@ func (transfer *TransferStructure) HandleSavePaths() { } } else { transfer.Fastresume.QBtContentLayout = "NoSubfolder" - // NoSubfolder always has full mapped files - // so we append all of them - for _, filePath := range transfer.TorrentFile.GetFileList() { - transfer.Fastresume.MappedFiles = append(transfer.Fastresume.MappedFiles, fileHelpers.Normalize(helpers.HandleCesu8(filePath), transfer.Opts.PathSeparator)) + // NoSubfolder always has full mapped files, so we append all of them + for _, filePath := range fileList { + transfer.Fastresume.MappedFiles = append(transfer.Fastresume.MappedFiles, + fileHelpers.Normalize(filePath, transfer.Opts.PathSeparator)) } // and then doing remap if resumeItem contain target field if maxIndex := transfer.FindHighestIndexOfMappedFiles(); maxIndex >= 0 { @@ -344,7 +334,7 @@ func (transfer *TransferStructure) HandleSavePaths() { } } else { transfer.Fastresume.QBtContentLayout = "Original" // utorrent\bittorrent don't support create subfolders for torrents with single file - if lastPathName != torrentNameOriginal { + if nameNormalized || lastPathName != transfer.Fastresume.Name { //it means that we have renamed path and targets item, and should have mapped files transfer.Fastresume.MappedFiles = []string{lastPathName} } diff --git a/internal/transfer/transfer_test.go b/internal/transfer/transfer_test.go index d603794..6360919 100644 --- a/internal/transfer/transfer_test.go +++ b/internal/transfer/transfer_test.go @@ -39,6 +39,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { QbtSavePath: `D:/torrents/`, SavePath: `D:\torrents\`, QBtContentLayout: "Original", + Name: `test_torrent.txt`, }, }, }, @@ -59,6 +60,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { QbtSavePath: `E:/newfolder/`, SavePath: `E:\newfolder\`, QBtContentLayout: "Original", + Name: `test_torrent.txt`, }, }, }, @@ -79,6 +81,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { QbtSavePath: `D:/torrents/`, SavePath: `D:\torrents\`, QBtContentLayout: "Original", + Name: `test_torrent.txt`, }, }, }, @@ -107,6 +110,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { QbtSavePath: `D:/torrents/`, SavePath: `D:\torrents\`, QBtContentLayout: "Original", + Name: `test_torrent.txt`, MappedFiles: []string{`renamed_test_torrent.txt`}, }, }, @@ -135,6 +139,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `/mnt/d/torrents/`, SavePath: `/mnt/d/torrents/`, + Name: `test_torrent.txt`, QBtContentLayout: "Original", MappedFiles: []string{`renamed_test_torrent.txt`}, }, @@ -162,6 +167,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { QbtSavePath: `D:/torrents/`, SavePath: `D:\torrents\`, QBtContentLayout: "Original", + Name: `test_torrent`, }, }, }, @@ -187,6 +193,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { QbtSavePath: `E:/newfolder/`, SavePath: `E:\newfolder\`, QBtContentLayout: "Original", + Name: `test_torrent`, }, }, }, @@ -212,6 +219,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { QbtSavePath: `D:/torrents/`, SavePath: `D:\torrents\`, QBtContentLayout: "Original", + Name: `test_torrent`, }, }, }, @@ -245,6 +253,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { QbtSavePath: `D:/torrents/`, SavePath: `D:\torrents\`, QBtContentLayout: "Original", + Name: `test_torrent`, MappedFiles: []string{ ``, ``, @@ -256,7 +265,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { { name: "010 Test torrent with windows folder (original) path with replace to linux paths and linux sep. Renamed File", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{Name: `test_torrent`}, ResumeItem: &utorrentStructs.ResumeItem{ Path: `D:\torrents\test_torrent`, Targets: [][]interface{}{ @@ -283,6 +292,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { QbtSavePath: `/mnt/d/torrents/`, SavePath: `/mnt/d/torrents/`, QBtContentLayout: "Original", + Name: `test_torrent`, MappedFiles: []string{ ``, ``, @@ -294,7 +304,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { { name: "011 Test torrent with windows folder (NoSubfolder) path without replaces", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{Name: `test_torrent`}, ResumeItem: &utorrentStructs.ResumeItem{Path: `D:\torrents\test`}, TorrentFile: &torrentStructures.Torrent{ Info: &torrentStructures.TorrentInfo{ @@ -312,6 +322,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `D:/torrents/test`, SavePath: `D:\torrents\test`, + Name: `test_torrent`, MappedFiles: []string{ `dir1\file1.txt`, `dir2\file2.txt`, @@ -324,7 +335,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { { name: "012 Test torrent with windows folder (NoSubfolder) path with replace", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{Name: `test_torrent`}, ResumeItem: &utorrentStructs.ResumeItem{Path: `D:\torrents\test`}, TorrentFile: &torrentStructures.Torrent{ Info: &torrentStructures.TorrentInfo{ @@ -342,6 +353,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `E:/newfolder/test`, SavePath: `E:\newfolder\test`, + Name: `test_torrent`, MappedFiles: []string{ `dir1\file1.txt`, `dir2\file2.txt`, @@ -354,7 +366,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { { name: "013 Test torrent with windows folder (NoSubfolder) path without replaces. NameUTF8", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{Name: `test_torrent`}, ResumeItem: &utorrentStructs.ResumeItem{Path: `D:\torrents\test`}, TorrentFile: &torrentStructures.Torrent{ Info: &torrentStructures.TorrentInfo{ @@ -372,6 +384,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `D:/torrents/test`, SavePath: `D:\torrents\test`, + Name: `test_torrent`, MappedFiles: []string{ `dir1\file1.txt`, `dir2\file2.txt`, @@ -384,7 +397,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { { name: "014 Test torrent with windows folder (NoSubfolder) path without replaces. Renamed File", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{Name: `test_torrent`}, ResumeItem: &utorrentStructs.ResumeItem{ Path: `D:\torrents\test`, Targets: [][]interface{}{ @@ -411,6 +424,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { QbtSavePath: `D:/torrents/test`, SavePath: `D:\torrents\test`, QBtContentLayout: "NoSubfolder", + Name: `test_torrent`, MappedFiles: []string{ `dir1\file1.txt`, `dir2\file2.txt`, @@ -422,7 +436,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { { name: "015 Test torrent with windows folder (NoSubfolder) path with replace to linux paths and linux sep. Renamed File", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{Name: `test_torrent`}, ResumeItem: &utorrentStructs.ResumeItem{ Path: `D:\torrents\test`, Targets: [][]interface{}{ @@ -448,6 +462,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `/mnt/d/torrents/test`, SavePath: `/mnt/d/torrents/test`, + Name: `test_torrent`, QBtContentLayout: "NoSubfolder", MappedFiles: []string{ `dir1/file1.txt`, @@ -478,6 +493,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `D:/torrents/test`, SavePath: `D:\torrents\test`, + Name: `test_torrent`, MappedFiles: []string{ `dir1\file1.txt`, `dir2\file2.txt`, @@ -491,7 +507,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { name: "017 Test torrent with windows single nofolder (original) path without replaces", mustFail: true, newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{Name: `test_torrent.txt`}, ResumeItem: &utorrentStructs.ResumeItem{Path: `D:\torrents\test_torrent.txt`}, TorrentFile: &torrentStructures.Torrent{ Info: &torrentStructures.TorrentInfo{ @@ -504,6 +520,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `D:/torrents/`, SavePath: `D:\torre`, + Name: `test_torrent.txt`, QBtContentLayout: "Original", }, }, @@ -511,7 +528,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { { name: "018 Test torrent with windows folder (original) path without replaces. Moved files with absolute paths", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{Name: `test_torrent`}, ResumeItem: &utorrentStructs.ResumeItem{ Path: `D:\torrents\test_torrent`, Targets: [][]interface{}{ @@ -547,6 +564,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `D:/torrents/`, SavePath: `D:\torrents\`, + Name: `test_torrent`, QBtContentLayout: "Original", MappedFiles: []string{ ``, @@ -561,7 +579,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { { name: "019 Test torrent with windows folder (original) path with replaces. Moved files with absolute paths", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{Name: `test_torrent`}, ResumeItem: &utorrentStructs.ResumeItem{ Path: `D:\torrents\test_torrent`, Targets: [][]interface{}{ @@ -597,6 +615,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `/mnt/d/torrents/`, SavePath: `/mnt/d/torrents/`, + Name: `test_torrent`, QBtContentLayout: "Original", MappedFiles: []string{ ``, @@ -611,7 +630,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { { name: "020 Test torrent with windows folder (NoSubfolder) path without replaces. Moved files with absolute paths", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{Name: `test_torrent`}, ResumeItem: &utorrentStructs.ResumeItem{ Path: `D:\torrents\test`, Targets: [][]interface{}{ @@ -647,6 +666,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `D:/torrents/test`, SavePath: `D:\torrents\test`, + Name: `test_torrent`, QBtContentLayout: "NoSubfolder", MappedFiles: []string{ `dir1\file1.txt`, @@ -661,7 +681,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { { name: "021 Test torrent with windows share folder (Original) path without replaces. Moved files with absolute paths. Windows share", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{Name: `test_torrent`}, ResumeItem: &utorrentStructs.ResumeItem{ Path: `\\torrents\test_torrent`, Targets: [][]interface{}{ @@ -697,6 +717,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `//torrents/`, SavePath: `\\torrents\`, + Name: `test_torrent`, QBtContentLayout: "Original", MappedFiles: []string{ ``, @@ -711,7 +732,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { { name: "022 Test torrent with windows share folder (NoSubfolder) path without replaces. Moved files with absolute paths", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{Name: `test_torrent`}, ResumeItem: &utorrentStructs.ResumeItem{ Path: `\\torrents\test`, Targets: [][]interface{}{ @@ -747,6 +768,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `//torrents/test`, SavePath: `\\torrents\test`, + Name: `test_torrent`, QBtContentLayout: "NoSubfolder", MappedFiles: []string{ `dir1\file1.txt`, @@ -761,7 +783,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { { name: "023 Test torrent with windows folder (Original) path without replaces. Absolute paths. Windows share Replace", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{Name: `test_torrent`}, ResumeItem: &utorrentStructs.ResumeItem{ Path: `\\torrents\test_torrent`, Targets: [][]interface{}{ @@ -797,6 +819,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `//torrents/`, SavePath: `//torrents/`, + Name: `test_torrent`, QBtContentLayout: "Original", MappedFiles: []string{ ``, @@ -847,6 +870,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `//torrents/test`, SavePath: `//torrents/test`, + Name: `test_torrent`, QBtContentLayout: "NoSubfolder", MappedFiles: []string{ `dir1/file1.txt`, @@ -866,7 +890,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Path: `D:\torrents\test`, }, TorrentFile: &torrentStructures.Torrent{ - Info: &torrentStructures.TorrentInfo{}, + Info: &torrentStructures.TorrentInfo{Name: "torrentname"}, }, Opts: &options.Opts{PathSeparator: `\`}, Magnet: true, @@ -875,6 +899,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `D:/torrents/test`, SavePath: `D:\torrents\test`, + Name: "torrentname", QBtContentLayout: "Original", }, }, @@ -897,6 +922,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `D:/`, SavePath: `D:\`, + Name: `test.txt`, QBtContentLayout: "Original", }, }, @@ -926,6 +952,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `D:/`, SavePath: `D:\`, + Name: `test_torrent`, QBtContentLayout: "Original", }, }, @@ -967,6 +994,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `D:/torrents/`, SavePath: `D:\torrents\`, + Name: "test_torrent \xf0\x9f\x86\x95", QBtContentLayout: "Original", MappedFiles: []string{ "E:\\somedir1 \xf0\x9f\x86\x95\\\xf0\x9f\x86\x95 renamed_test_torrent2.txt", @@ -997,6 +1025,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: "D:/torrents/renamed test_torrent \xf0\x9f\x86\x95", SavePath: "D:\\torrents\\renamed test_torrent \xf0\x9f\x86\x95", + Name: "test_torrent \xf0\x9f\x86\x95", QBtContentLayout: "NoSubfolder", MappedFiles: []string{ "dir1\\\xf0\x9f\x86\x95 file1.txt", @@ -1043,6 +1072,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: "D:/torrents/test_torrent \xf0\x9f\x86\x95", SavePath: "D:\\torrents\\test_torrent \xf0\x9f\x86\x95", + Name: "test_torrent \xf0\x9f\x86\x95", QBtContentLayout: "NoSubfolder", MappedFiles: []string{ "E:\\somedir1 \xf0\x9f\x86\x95\\\xf0\x9f\x86\x95 renamed_test_torrent2.txt", @@ -1073,6 +1103,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: "D:/torrents/renamed test_torrent \xf0\x9f\x86\x95", SavePath: "D:\\torrents\\renamed test_torrent \xf0\x9f\x86\x95", + Name: "test_torrent \xf0\x9f\x86\x95", QBtContentLayout: "NoSubfolder", MappedFiles: []string{ "dir1\\\xf0\x9f\x86\x95 file1.txt", @@ -1105,6 +1136,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: "D:/torrents/test_torrent", SavePath: "D:\\torrents\\test_torrent", + Name: "test_torrent", QBtContentLayout: "NoSubfolder", MappedFiles: []string{ "dir1\\\xf0\x9f\x86\x95 file1.txt", @@ -1156,6 +1188,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: "D:/torrents/test_torrent", SavePath: "D:\\torrents\\test_torrent", + Name: "test_torrent", QBtContentLayout: "NoSubfolder", MappedFiles: []string{ "E:\\somedir1 \xf0\x9f\x86\x95\\\xf0\x9f\x86\x95 renamed_test_torrent2.txt", @@ -1184,6 +1217,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `D:/torrents/`, SavePath: `D:\torrents\`, + Name: "test_torrent \xf0\x9f\x86\x95.txt", MappedFiles: []string{ "test_torrent \xf0\x9f\x86\x95.txt", }, @@ -1211,6 +1245,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{ QbtSavePath: `D:/torrents/renamed test_torrent`, SavePath: `D:\torrents\renamed test_torrent`, + Name: `test_torrent`, QBtContentLayout: "NoSubfolder", MappedFiles: []string{ `#test _ test [01]{1} [6K].jpg`, @@ -1220,6 +1255,435 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { Opts: &options.Opts{PathSeparator: `\`}, }, }, + { + name: "035 Test torrent with windows folder (NoSubfolder) with prohibited symbols.", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{Path: `D:\torrents\renamed test_torrent`}, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_torrent", + Files: []*torrentStructures.TorrentFile{ + &torrentStructures.TorrentFile{Path: []string{`#test | test [01]{1} [6K].jpg`}}, + &torrentStructures.TorrentFile{Path: []string{`testdir1 collection`, `testdir2?`, `1.jpg`}}, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: `D:/torrents/renamed test_torrent`, + SavePath: `D:\torrents\renamed test_torrent`, + Name: `test_torrent`, + QBtContentLayout: "NoSubfolder", + MappedFiles: []string{ + `#test _ test [01]{1} [6K].jpg`, + `testdir1 collection\testdir2_\1.jpg`, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + }, + { + name: "036 Test torrent with windows single nofolder (original) path without replaces. With prohibited symbols", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{Path: `D:\torrents\test_torrent.txt`}, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: `test|torrent.txt`, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + Name: `test_torrent.txt`, + QbtSavePath: `D:/torrents/`, + SavePath: `D:\torrents\`, + QBtContentLayout: "Original", + MappedFiles: []string{ + `test_torrent.txt`, + }, + }, + }, + }, + { + name: "037 Test torrent with windows folder (NoSubfolder) with prohibited symbols. *nix path separator", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{Path: `D:\torrents\renamed test_torrent`}, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_torrent", + Files: []*torrentStructures.TorrentFile{ + &torrentStructures.TorrentFile{Path: []string{`#test | test [01]{1} [6K].jpg`}}, + &torrentStructures.TorrentFile{Path: []string{`testdir1 collection`, `testdir2?`, `1.jpg`}}, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `/`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: `D:/torrents/renamed test_torrent`, + SavePath: `D:/torrents/renamed test_torrent`, + Name: `test_torrent`, + QBtContentLayout: "NoSubfolder", + MappedFiles: []string{ + `#test _ test [01]{1} [6K].jpg`, + `testdir1 collection/testdir2_/1.jpg`, + }, + }, + }, + }, + { + name: "038 Test torrent with windows single nofolder (original) path without replaces. With prohibited symbols. *nix path separator", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{Path: `D:\torrents\test_torrent.txt`}, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: `test|torrent.txt`, + }, + }, + Opts: &options.Opts{PathSeparator: `/`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: `D:/torrents/`, + SavePath: `D:/torrents/`, + Name: `test_torrent.txt`, + QBtContentLayout: "Original", + MappedFiles: []string{ + `test_torrent.txt`, + }, + }, + }, + }, + { + name: "039 Test torrent with windows single nofolder (original) path with renamed file. v2 torrent", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{ + Path: `D:\torrents\testtorrent.txt`, + Targets: [][]interface{}{ + []interface{}{ + int64(0), + "testtorrent.txt", + }, + }, + }, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: `testtorrent.txt`, + FileTree: map[string]interface{}{ + `testtorrent.txt`: map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(100), + `pieces_root`: []byte{}, + }, + }, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: `D:/torrents/`, + SavePath: `D:\torrents\`, + Name: `testtorrent.txt`, + QBtContentLayout: "Original", + }, + }, + }, + { + name: "040 Test torrent with windows folder (original) path with renamed file. v2 torrent", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{ + Path: `D:\torrents\testtorrent`, + Targets: [][]interface{}{ + []interface{}{ + int64(1), + "testtorrent2.txt", + }, + }, + }, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: `testtorrent`, + FileTree: map[string]interface{}{ + `testtorrent1.txt`: map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(100), + `pieces_root`: []byte{}, + }, + }, + `testtorrent3.txt`: map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(100), + `pieces_root`: []byte{}, + }, + }, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: `D:/torrents/`, + SavePath: `D:\torrents\`, + Name: `testtorrent`, + QBtContentLayout: "Original", + MappedFiles: []string{ + ``, + `testtorrent\testtorrent2.txt`, + }, + }, + }, + }, + { + name: "041 Test torrent with multi file torrent (NoSubfolder) with prohibited symbols in name", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{ + Path: `D:\test_torrent_test`, + }, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_torrent/test", + Files: []*torrentStructures.TorrentFile{ + &torrentStructures.TorrentFile{Path: []string{"dir1", "file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"dir2", "file2.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file0.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file2.txt"}}, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: `D:/test_torrent_test`, + SavePath: `D:\test_torrent_test`, + Name: `test_torrent_test`, + QBtContentLayout: "NoSubfolder", + MappedFiles: []string{ + `dir1\file1.txt`, + `dir2\file2.txt`, + `file0.txt`, + `file1.txt`, + `file2.txt`, + }, + }, + }, + }, + { + name: "042 Test torrent with multi file torrent with renames (NoSubFolder) with prohibited symbols in name", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{ + Path: `D:\test_torrent_test`, + Targets: [][]interface{}{ + []interface{}{ + int64(2), + "file0_file.txt", + }, + }, + }, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_torrent/test", + Files: []*torrentStructures.TorrentFile{ + &torrentStructures.TorrentFile{Path: []string{"dir1", "file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"dir2", "file2.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file0.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file2.txt"}}, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: `D:/test_torrent_test`, + SavePath: `D:\test_torrent_test`, + Name: `test_torrent_test`, + QBtContentLayout: "NoSubfolder", + MappedFiles: []string{ + `dir1\file1.txt`, + `dir2\file2.txt`, + `file0_file.txt`, + `file1.txt`, + `file2.txt`, + }, + }, + }, + }, + { + name: "043 Test torrent with multi file torrent (NoSubfolder) with space at the end of torrent name", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{ + Path: `D:\test_torrent_`, + }, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_torrent ", + Files: []*torrentStructures.TorrentFile{ + &torrentStructures.TorrentFile{Path: []string{"dir1", "file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"dir2", "file2.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file0.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file2.txt"}}, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: `D:/test_torrent_`, + SavePath: `D:\test_torrent_`, + Name: `test_torrent_`, + QBtContentLayout: "NoSubfolder", + MappedFiles: []string{ + `dir1\file1.txt`, + `dir2\file2.txt`, + `file0.txt`, + `file1.txt`, + `file2.txt`, + }, + }, + }, + }, + { + name: "044 Test torrent with multi file torrent (NoSubfolder) with space at the end directory", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{ + Path: `D:\test_torrent_`, + }, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_torrent ", + Files: []*torrentStructures.TorrentFile{ + &torrentStructures.TorrentFile{Path: []string{"file0.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file2.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"dir1 ", "dir2 ", "file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"dir3 ", "file2.txt"}}, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: `D:/test_torrent_`, + SavePath: `D:\test_torrent_`, + Name: `test_torrent_`, + QBtContentLayout: "NoSubfolder", + MappedFiles: []string{ + `file0.txt`, + `file1.txt`, + `file2.txt`, + `dir1_\dir2_\file1.txt`, + `dir3_\file2.txt`, + }, + }, + }, + }, + { + name: "045 Test torrent with multi file torrent (NoSubfolder) with space at the end directory with renamed files", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{ + Path: `D:\test_torrent_`, + Targets: [][]interface{}{ + []interface{}{ + int64(2), + "file2_renamed.txt", + }, + }, + }, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_torrent ", + Files: []*torrentStructures.TorrentFile{ + &torrentStructures.TorrentFile{Path: []string{"file0.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file2.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"dir1 ", "dir2 ", "file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"dir3 ", "file2.txt"}}, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: `D:/test_torrent_`, + SavePath: `D:\test_torrent_`, + Name: `test_torrent_`, + QBtContentLayout: "NoSubfolder", + MappedFiles: []string{ + `file0.txt`, + `file1.txt`, + `file2_renamed.txt`, + `dir1_\dir2_\file1.txt`, + `dir3_\file2.txt`, + }, + }, + }, + }, + { + name: "046 Test torrent with multi file torrent with transfer to NoSubfolder cesu8 symbols in names", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{ + Path: "D:\\test_slashes_emoji \xed\xa0\xbc\xed\xb6\x95_", + Caption: "test_slashes_emoji \xed\xa0\xbc\xed\xb6\x95_", + }, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_slashes/emoji \xed\xa0\xbc\xed\xb6\x95 ", + Files: []*torrentStructures.TorrentFile{ + &torrentStructures.TorrentFile{Path: []string{"file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file_with/slash.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"testdir_with_emoji_and_space \xed\xa0\xbc\xed\xb6\x95 ", "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"testdir_with_emoji_and_space \xed\xa0\xbc\xed\xb6\x95 ", "file_with/slash.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"testdir_with_space ", "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"testdir_with_space ", "file_with/slash.txt"}}, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: "D:/test_slashes_emoji \xf0\x9f\x86\x95_", + SavePath: "D:\\test_slashes_emoji \xf0\x9f\x86\x95_", + Name: "test_slashes_emoji \xf0\x9f\x86\x95_", + QbtName: "test_slashes_emoji \xf0\x9f\x86\x95_", + QBtContentLayout: `NoSubfolder`, + MappedFiles: []string{ + "file_with_emoji \xf0\x9f\x86\x95.txt", + "file_with_slash.txt", + "testdir_with_emoji_and_space \xf0\x9f\x86\x95_\\file_with_emoji \xf0\x9f\x86\x95.txt", + "testdir_with_emoji_and_space \xf0\x9f\x86\x95_\\file_with_slash.txt", + "testdir_with_space_\\file_with_emoji \xf0\x9f\x86\x95.txt", + "testdir_with_space_\\file_with_slash.txt", + }, + }, + }, + }, } for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { @@ -1228,6 +1692,8 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { testCase.newTransferStructure.Replace = replaces testCase.expected.Replace = replaces } + testCase.newTransferStructure.Fastresume.Name, _ = testCase.newTransferStructure.TorrentFile.GetNormalizedTorrentName() + testCase.newTransferStructure.HandleCaption() testCase.newTransferStructure.HandleSavePaths() equal := reflect.DeepEqual(testCase.expected.Fastresume, testCase.newTransferStructure.Fastresume) if !equal && !testCase.mustFail { @@ -1235,9 +1701,9 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { if err != nil { t.Error(err.Error()) } - t.Fatalf("Unexpected error: opts isn't equal:\n Got: %#v \n Expect %#v \n Diff: %v\n", testCase.newTransferStructure.Fastresume, testCase.expected.Fastresume, spew.Sdump(changes)) + t.Fatalf("Unexpected error: opts isn't equal:\nGot: %#v\nExpect %#v\nDiff: %v\n", testCase.newTransferStructure.Fastresume, testCase.expected.Fastresume, spew.Sdump(changes)) } else if equal && testCase.mustFail { - t.Fatalf("Unexpected error: structures are equal, but they shouldn't\n Got: %v\n", spew.Sdump(testCase.newTransferStructure.Fastresume)) + t.Fatalf("Unexpected error: structures are equal, but they shouldn't\nGot: %v\n", spew.Sdump(testCase.newTransferStructure.Fastresume)) } }) } diff --git a/pkg/fileHelpers/file_test.go b/pkg/fileHelpers/file_test.go index 53eda6d..6318b16 100644 --- a/pkg/fileHelpers/file_test.go +++ b/pkg/fileHelpers/file_test.go @@ -129,7 +129,7 @@ func TestJoin(t *testing.T) { expected: `..\..\testdir\my test file.txt`, }, { - name: "009 windows relative paths join. check normalize", + name: "009 linux relative paths join. check normalize", paths: []string{`./testdir/../testdir/../testdir/./`, `my test file.txt`}, separator: `/`, expected: `testdir/my test file.txt`, diff --git a/pkg/helpers/helpers.go b/pkg/helpers/helpers.go index 44bee4d..a0ffcbd 100644 --- a/pkg/helpers/helpers.go +++ b/pkg/helpers/helpers.go @@ -6,8 +6,8 @@ import ( "github.com/crazytyper/go-cesu8" "github.com/zeebo/bencode" "io" - "io/ioutil" "os" + "regexp" "strconv" "strings" ) @@ -35,7 +35,7 @@ func CheckExists(s string, arr []string) (bool, string) { } func DecodeTorrentFile(path string, decodeTo interface{}) error { - dat, err := ioutil.ReadFile(path) + dat, err := os.ReadFile(path) if err != nil { return err } @@ -119,3 +119,9 @@ func HandleCesu8(str string) string { } return str } + +// ReplaceAllSymbols Replace all symbols in set to replacer +func ReplaceAllSymbols(str string, set string, replacer string) string { + re := regexp.MustCompilePOSIX(`[` + regexp.QuoteMeta(set) + `]`) + return re.ReplaceAllString(str, replacer) +} diff --git a/pkg/helpers/helpers_test.go b/pkg/helpers/helpers_test.go index 9a8dca3..610c87b 100644 --- a/pkg/helpers/helpers_test.go +++ b/pkg/helpers/helpers_test.go @@ -57,6 +57,7 @@ func TestDecodeTorrentFile(t *testing.T) { }) } } + func TestEmojiCesu8(t *testing.T) { cesu8 := "normal_text \xed\xa0\xbc\xed\xb6\x95 normal_text \xed\xa0\xbd\xed\xba\x9c.txt.torrent" utf8 := "normal_text \xf0\x9f\x86\x95 normal_text \xf0\x9f\x9a\x9c.txt.torrent" @@ -64,3 +65,71 @@ func TestEmojiCesu8(t *testing.T) { t.Fatalf("Cesu8 to utf-8 transformation fail") } } +func TestReplaceAllSymbols(t *testing.T) { + type Case struct { + name string + str string + set string + replacer string + expected string + } + cases := []Case{ + { + name: "001 one symbol", + str: `qwerty`, + set: `qry`, + replacer: `_`, + expected: `_we_t_`, + }, + { + name: "002 several replacer symbol", + str: `qwerty`, + set: `qry`, + replacer: `AAA`, + expected: `AAAweAAAtAAA`, + }, + { + name: "003 several replacer symbol that exists in str", + str: `qwerty`, + set: `qry`, + replacer: `qwerty`, + expected: `qwertyweqwertytqwerty`, + }, + { + name: "004 several replacer symbol that exists in str with special symbols", + str: `[qwerty]`, + set: `[qry]`, + replacer: `qwerty`, + expected: `qwertyqwertyweqwertytqwertyqwerty`, + }, + { + name: "005 several replacer symbol that exists in str with special symbols", + str: `[qwerty]`, + set: `[qry]`, + replacer: `[qwerty]`, + expected: `[qwerty][qwerty]we[qwerty]t[qwerty][qwerty]`, + }, + { + name: "006 emoji replace", + str: `qwer🚎y`, + set: `🚎`, + replacer: `_`, + expected: `qwer_y`, + }, + { + name: "006 two emoji replace", + str: `qwer🚎y👍`, + set: `🚎😊`, + replacer: `_`, + expected: `qwer_y👍`, + }, + } + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + replaced := ReplaceAllSymbols(testCase.str, testCase.set, testCase.replacer) + if replaced != testCase.expected { + t.Fatalf("Unexpected error:\nstr: %v set: %v replacer: %v\nGot: %v\nExpect %v\n", testCase.str, testCase.set, testCase.replacer, replaced, testCase.expected) + } + }) + } +} diff --git a/pkg/normalization/normalization.go b/pkg/normalization/normalization.go new file mode 100644 index 0000000..cb7a3c2 --- /dev/null +++ b/pkg/normalization/normalization.go @@ -0,0 +1,35 @@ +package normalization + +import ( + "github.com/rumanzo/bt2qbt/pkg/helpers" + "regexp" +) + +// ProhibitedSymbolsStrict we can't use these symbols on Windows systems, but can use in *nix +var ProhibitedSymbolsStrict = regexp.MustCompilePOSIX(`[\\/:*?"<>|]`) + +func NormalizeSpaceEnding(str string) (string, bool) { + var normalized bool + if string(str[len(str)-1]) == ` ` { + str = str[:len(str)-1] + `_` + normalized = true + } + return str, normalized +} + +func FullNormalize(str string) (string, bool) { + var normalized bool + s1 := ProhibitedSymbolsStrict.ReplaceAllString(str, `_`) + if s1 != str { + normalized = true + } + s2 := helpers.HandleCesu8(s1) + if s1 != s2 { + normalized = true + } + s3, n := NormalizeSpaceEnding(s2) + if n { + normalized = true + } + return s3, normalized +} diff --git a/pkg/torrentStructures/functions.go b/pkg/torrentStructures/functions.go index 215174e..186e9b6 100644 --- a/pkg/torrentStructures/functions.go +++ b/pkg/torrentStructures/functions.go @@ -2,6 +2,8 @@ package torrentStructures import ( "github.com/rumanzo/bt2qbt/pkg/fileHelpers" + "github.com/rumanzo/bt2qbt/pkg/helpers" + "github.com/rumanzo/bt2qbt/pkg/normalization" "sort" ) @@ -12,26 +14,49 @@ func (t *Torrent) IsV2OrHybryd() bool { return false } -// GetFileListWB function that return struct with filelists with bytes from torrent file -func (t *Torrent) GetFileListWB() []FilepathLength { - if t.FilePathLength == nil { - if t.IsV2OrHybryd() { // torrents with v2 or hybrid scheme - result := getFileListV2(t.Info.FileTree) - t.FilePathLength = &result - return *t.FilePathLength - } else { // torrent v1 with FileTree - result := getFileListV1(t) - t.FilePathLength = &result - return *t.FilePathLength +func (t *Torrent) IsSingle() bool { + if t.Single != nil { + return *t.Single + } + single := false + if t.IsV2OrHybryd() { + // v2 torrents always have at least one file that equal torrent name + if len(t.Info.FileTree) == 1 { + torrentName, _ := t.GetNormalizedTorrentName() + if _, ok := t.Info.FileTree[torrentName]; ok { + single = true + } } } else { - return *t.FilePathLength + if t.Info.Files == nil { + single = true + } + } + t.Single = &single + return *t.Single +} + +// GetFileListWB function that return struct with filelists with bytes from torrent file +func (t *Torrent) GetFileListWB() ([]FilepathLength, bool) { + if t.FilePathLength == nil { + if t.IsV2OrHybryd() { // torrents with v2 or hybrid scheme + result, normalized := getFileListV2(t.Info.FileTree) + t.FilePathLength = &result + return *t.FilePathLength, normalized + } else { // torrent v1 with FileTree + result, normalized := getFileListV1(t) + t.FilePathLength = &result + return *t.FilePathLength, normalized + } + } else { + return *t.FilePathLength, false } } -func (t *Torrent) GetFileList() []string { +func (t *Torrent) GetFileList() ([]string, bool) { + var normalized bool if t.FilePathLength == nil { - t.GetFileListWB() + _, normalized = t.GetFileListWB() } if t.FilePaths == nil { t.FilePaths = &[]string{} @@ -39,29 +64,38 @@ func (t *Torrent) GetFileList() []string { *t.FilePaths = append(*t.FilePaths, fb.Path) } } - return *t.FilePaths + return *t.FilePaths, normalized } -func getFileListV1(t *Torrent) []FilepathLength { +func getFileListV1(t *Torrent) ([]FilepathLength, bool) { + var normalized bool var files []FilepathLength - for _, file := range t.Info.Files { - if file.PathUTF8 != nil { - files = append(files, FilepathLength{ - Path: fileHelpers.Join(file.PathUTF8, `/`), - Length: file.Length, - }) + for _, fileList := range t.Info.Files { + + var normalizedFileList []string + if fileList.PathUTF8 != nil { + normalizedFileList = fileList.PathUTF8 } else { - files = append(files, FilepathLength{ - Path: fileHelpers.Join(file.Path, `/`), - Length: file.Length, - }) + normalizedFileList = fileList.Path } + for index, filePathPart := range normalizedFileList { + normalizedFilePathPart, gotNormalized := normalization.FullNormalize(filePathPart) + if gotNormalized { + normalized = true + normalizedFileList[index] = normalizedFilePathPart + } + } + files = append(files, FilepathLength{ + Path: fileHelpers.Join(normalizedFileList, `/`), + Length: fileList.Length, + }) } - return files + return files, normalized } -func getFileListV2(f interface{}) []FilepathLength { - nfiles := []FilepathLength{} +func getFileListV2(f interface{}) ([]FilepathLength, bool) { + var normalized bool + var nfiles []FilepathLength // sort map previously keys := make([]string, 0, len(f.(map[string]interface{}))) @@ -74,12 +108,39 @@ func getFileListV2(f interface{}) []FilepathLength { v := f.(map[string]interface{})[k] if len(k) == 0 { // it's means that next will be structure with length and piece root nfiles = append(nfiles, FilepathLength{Path: "", Length: v.(map[string]interface{})["length"].(int64)}) - return nfiles + return nfiles, normalized + } + s, gotNormalized := getFileListV2(v) + if gotNormalized { + normalized = true } - s := getFileListV2(v) for _, fpl := range s { - nfiles = append(nfiles, FilepathLength{Path: fileHelpers.Join(append([]string{k}, fpl.Path), `/`), Length: fpl.Length}) + normalizedPath, gotNormalized := normalization.FullNormalize(k) + if gotNormalized { + normalized = true + } + nfiles = append(nfiles, FilepathLength{Path: fileHelpers.Join(append([]string{normalizedPath}, fpl.Path), `/`), Length: fpl.Length}) } } - return nfiles + return nfiles, normalized +} + +func (t *Torrent) GetTorrentName() string { + if t.Info.NameUTF8 != "" { + return t.Info.NameUTF8 + } else { + return t.Info.Name + } +} + +func (t *Torrent) GetNormalizedTorrentName() (string, bool) { + torrentName := t.GetTorrentName() + var normalizedTorrentName string + var normalized bool + if fileHelpers.IsAbs(torrentName) { + normalizedTorrentName, normalized = normalization.NormalizeSpaceEnding(helpers.HandleCesu8(torrentName)) + } else { + normalizedTorrentName, normalized = normalization.FullNormalize(torrentName) + } + return normalizedTorrentName, normalized } diff --git a/pkg/torrentStructures/functions_test.go b/pkg/torrentStructures/functions_test.go index 4823cc7..a572f56 100644 --- a/pkg/torrentStructures/functions_test.go +++ b/pkg/torrentStructures/functions_test.go @@ -48,6 +48,10 @@ func TestDecodeRealTorrents(t *testing.T) { name: "008 single v2", path: "../../test/data/testfile1_single_v2.torrent", }, + { + name: "008 multi emoji prohibited symbols", + path: "../../test/data/tests_slahes_emoji_ut_created.torrent", + }, } for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { @@ -129,7 +133,7 @@ func TestTorrent_GetFileList(t *testing.T) { if err != nil { t.Fatalf("Unexpected error with decoding torrent file: %v", err) } - list := torrent.GetFileList() + list, _ := torrent.GetFileList() equal := reflect.DeepEqual(list, testCase.expected) if !equal && !testCase.mustFail { changes, err := diff.Diff(list, testCase.expected, diff.DiscardComplexOrigin()) @@ -203,6 +207,21 @@ func TestTorrent_GetFileListWB(t *testing.T) { FilepathLength{Path: "testfile3.txt", Length: 33}, }, }, + { + name: "004 testdir hybrid", + path: "../../test/data/testdir_hybrid.torrent", + expected: []FilepathLength{ + FilepathLength{Path: "dir1/testfile1.txt", Length: 33}, + FilepathLength{Path: "dir2/testfile1.txt", Length: 33}, + FilepathLength{Path: "dir2/testfile2.txt", Length: 33}, + FilepathLength{Path: "dir3/testfile1.txt", Length: 33}, + FilepathLength{Path: "dir3/testfile2.txt", Length: 33}, + FilepathLength{Path: "dir3/testfile3.txt", Length: 33}, + FilepathLength{Path: "testfile1.txt", Length: 33}, + FilepathLength{Path: "testfile2.txt", Length: 33}, + FilepathLength{Path: "testfile3.txt", Length: 33}, + }, + }, } for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { @@ -211,7 +230,7 @@ func TestTorrent_GetFileListWB(t *testing.T) { if err != nil { t.Fatalf("Unexpected error with decoding torrent file: %v", err) } - list := torrent.GetFileListWB() + list, _ := torrent.GetFileListWB() equal := reflect.DeepEqual(list, testCase.expected) if !equal && !testCase.mustFail { changes, err := diff.Diff(list, testCase.expected, diff.DiscardComplexOrigin()) @@ -225,3 +244,344 @@ func TestTorrent_GetFileListWB(t *testing.T) { }) } } + +func TestTorrent_GetFileListV1(t *testing.T) { + type TestCases struct { + name string + torrent *Torrent + expected []FilepathLength + expectedNormalized bool + mustFail bool + } + cases := []TestCases{ + { + name: "001 emoji spaces at dir ends prohibited symbols", + torrent: &Torrent{Info: &TorrentInfo{ + Files: []*TorrentFile{ + &TorrentFile{Path: []string{"file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt"}, Length: 5}, + &TorrentFile{Path: []string{"file_with/slash.txt"}, Length: 5}, + &TorrentFile{Path: []string{"testdir_with_emoji_and_space \xed\xa0\xbc\xed\xb6\x95 ", "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt"}, Length: 5}, + &TorrentFile{Path: []string{"testdir_with_emoji_and_space \xed\xa0\xbc\xed\xb6\x95 ", "file_with/slash.txt"}, Length: 5}, + &TorrentFile{Path: []string{"testdir_with_space ", "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt"}, Length: 5}, + &TorrentFile{Path: []string{"testdir_with_space ", "file_with/slash.txt"}, Length: 5}, + }, + }}, + expectedNormalized: true, + expected: []FilepathLength{ + {Path: "file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "file_with_slash.txt", Length: 5}, + {Path: "testdir_with_emoji_and_space \xf0\x9f\x86\x95_/file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "testdir_with_emoji_and_space \xf0\x9f\x86\x95_/file_with_slash.txt", Length: 5}, + {Path: "testdir_with_space_/file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "testdir_with_space_/file_with_slash.txt", Length: 5}, + }, + mustFail: false, + }, + { + name: "002 mustFail wrong filename", + torrent: &Torrent{Info: &TorrentInfo{ + Files: []*TorrentFile{ + &TorrentFile{Path: []string{"file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt"}, Length: 5}, + &TorrentFile{Path: []string{"file_with/slash.txt"}, Length: 5}, + &TorrentFile{Path: []string{"testdir_with_emoji_and_space \xed\xa0\xbc\xed\xb6\x95 ", "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt"}, Length: 5}, + &TorrentFile{Path: []string{"testdir_with_emoji_and_space \xed\xa0\xbc\xed\xb6\x95 ", "file_with/slash.txt"}, Length: 5}, + &TorrentFile{Path: []string{"testdir_with_space ", "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt"}, Length: 5}, + &TorrentFile{Path: []string{"testdir_with_space ", "file_with/slash.txt"}, Length: 5}, + }, + }}, + expectedNormalized: true, + expected: []FilepathLength{ + {Path: "file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "file_with_slash.txt", Length: 5}, + {Path: "testdir_with_emoji_and_space \xf0\x9f\x86\x95_/file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "testdir_with_emoji_and_space \xf0\x9f\x86\x95_/file_with_slash.txt", Length: 5}, + {Path: "testdir_with_space_/file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "testdir_with_space_/file__with_slash.txt", Length: 5}, + }, + mustFail: true, + }, + { + name: "003 emoji spaces at dir ends prohibited symbols mustfail due returned false normalized", + torrent: &Torrent{Info: &TorrentInfo{ + Files: []*TorrentFile{ + &TorrentFile{Path: []string{"file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt"}, Length: 5}, + &TorrentFile{Path: []string{"file_with/slash.txt"}, Length: 5}, + &TorrentFile{Path: []string{"testdir_with_emoji_and_space \xed\xa0\xbc\xed\xb6\x95 ", "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt"}, Length: 5}, + &TorrentFile{Path: []string{"testdir_with_emoji_and_space \xed\xa0\xbc\xed\xb6\x95 ", "file_with/slash.txt"}, Length: 5}, + &TorrentFile{Path: []string{"testdir_with_space ", "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt"}, Length: 5}, + &TorrentFile{Path: []string{"testdir_with_space ", "file_with/slash.txt"}, Length: 5}, + }, + }}, + expectedNormalized: false, + expected: []FilepathLength{ + {Path: "file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "file_with_slash.txt", Length: 5}, + {Path: "testdir_with_emoji_and_space \xf0\x9f\x86\x95_/file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "testdir_with_emoji_and_space \xf0\x9f\x86\x95_/file_with_slash.txt", Length: 5}, + {Path: "testdir_with_space_/file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "testdir_with_space_/file_with_slash.txt", Length: 5}, + }, + mustFail: true, + }, + { + name: "004 normal torrent", + torrent: &Torrent{Info: &TorrentInfo{ + Files: []*TorrentFile{ + &TorrentFile{Path: []string{"file.txt"}, Length: 5}, + &TorrentFile{Path: []string{"testdir", "file.txt"}, Length: 5}, + }, + }}, + expectedNormalized: false, + expected: []FilepathLength{ + {Path: "file.txt", Length: 5}, + {Path: "testdir/file.txt", Length: 5}, + }, + mustFail: false, + }, + } + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + filePathLength, normalized := getFileListV1(testCase.torrent) + equal := reflect.DeepEqual(filePathLength, testCase.expected) + if !equal && !testCase.mustFail { + changes, err := diff.Diff(filePathLength, testCase.expected, diff.DiscardComplexOrigin()) + if err != nil { + t.Error(err.Error()) + } + t.Fatalf("Unexpected error: structures isn't equal:\n Got: %#v\n Expect %#v\n Diff: %v\n", filePathLength, testCase.expected, spew.Sdump(changes)) + } + if normalized != testCase.expectedNormalized && !testCase.mustFail { + t.Fatalf("Normalization expected %v, got %v", testCase.expectedNormalized, normalized) + } + }) + } +} +func TestTorrent_GetFileListV2(t *testing.T) { + type TestCases struct { + name string + torrent *Torrent + expected []FilepathLength + expectedNormalized bool + mustFail bool + } + cases := []TestCases{ + { + name: "001 emoji spaces at dir ends prohibited symbols", + torrent: &Torrent{Info: &TorrentInfo{ + FileTree: map[string]interface{}{ + "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + "file_with/slash.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + "testdir_with_emoji_and_space \xed\xa0\xbc\xed\xb6\x95 ": map[string]interface{}{ + "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + "file_with/slash.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + }, + "testdir_with_space ": map[string]interface{}{ + "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + "file_with/slash.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + }, + }, + }}, + expectedNormalized: true, + expected: []FilepathLength{ + {Path: "file_with_slash.txt", Length: 5}, + {Path: "file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "testdir_with_emoji_and_space \xf0\x9f\x86\x95_/file_with_slash.txt", Length: 5}, + {Path: "testdir_with_emoji_and_space \xf0\x9f\x86\x95_/file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "testdir_with_space_/file_with_slash.txt", Length: 5}, + {Path: "testdir_with_space_/file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + }, + mustFail: false, + }, + { + name: "002 mustFail wrong filename", + torrent: &Torrent{Info: &TorrentInfo{ + FileTree: map[string]interface{}{ + "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + "file_with/slash.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + "testdir_with_emoji_and_space \xed\xa0\xbc\xed\xb6\x95 ": map[string]interface{}{ + "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + "file_with/slash.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + }, + "testdir_with_space ": map[string]interface{}{ + "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + "file_with/slash.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + }, + }, + }}, + expectedNormalized: true, + expected: []FilepathLength{ + {Path: "file_with_slash.txt", Length: 5}, + {Path: "file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "testdir_with_emoji_and_space \xf0\x9f\x86\x95_/file_with_slash.txt", Length: 5}, + {Path: "testdir_with_emoji_and_space \xf0\x9f\x86\x95_/file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "testdir_with_space_/file_with_slash.txt", Length: 5}, + {Path: "testdir_with_space_/file_with_emoji_ \xf0\x9f\x86\x95.txt", Length: 5}, + }, + mustFail: true, + }, + { + name: "003 emoji spaces at dir ends prohibited symbols mustfail due returned false normalized", + torrent: &Torrent{Info: &TorrentInfo{ + FileTree: map[string]interface{}{ + "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + "file_with/slash.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + "testdir_with_emoji_and_space \xed\xa0\xbc\xed\xb6\x95 ": map[string]interface{}{ + "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + "file_with/slash.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + }, + "testdir_with_space ": map[string]interface{}{ + "file_with_emoji \xed\xa0\xbc\xed\xb6\x95.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + "file_with/slash.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + }, + }, + }}, + expectedNormalized: false, + expected: []FilepathLength{ + {Path: "file_with_slash.txt", Length: 5}, + {Path: "file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "testdir_with_emoji_and_space \xf0\x9f\x86\x95_/file_with_slash.txt", Length: 5}, + {Path: "testdir_with_emoji_and_space \xf0\x9f\x86\x95_/file_with_emoji \xf0\x9f\x86\x95.txt", Length: 5}, + {Path: "testdir_with_space_/file_with_slash.txt", Length: 5}, + {Path: "testdir_with_space_/file_with_emoji_ \xf0\x9f\x86\x95.txt", Length: 5}, + }, + mustFail: true, + }, + { + name: "004 normal torrent", + torrent: &Torrent{Info: &TorrentInfo{ + Files: []*TorrentFile{ + &TorrentFile{Path: []string{"file.txt"}, Length: 5}, + &TorrentFile{Path: []string{"testdir", "file.txt"}, Length: 5}, + }, + FileTree: map[string]interface{}{ + "file.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + "testdir": map[string]interface{}{ + "file.txt": map[string]interface{}{ + ``: map[string]interface{}{ + `length`: int64(5), + `pieces_root`: []byte{}, + }, + }, + }, + }, + }}, + expectedNormalized: false, + expected: []FilepathLength{ + {Path: "file.txt", Length: 5}, + {Path: "testdir/file.txt", Length: 5}, + }, + mustFail: false, + }, + } + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + filePathLength, normalized := getFileListV2(testCase.torrent.Info.FileTree) + equal := reflect.DeepEqual(filePathLength, testCase.expected) + if !equal && !testCase.mustFail { + changes, err := diff.Diff(filePathLength, testCase.expected, diff.DiscardComplexOrigin()) + if err != nil { + t.Error(err.Error()) + } + t.Fatalf("Unexpected error: structures isn't equal:\n Got: %#v\nExpected: %#v\nDiff: %v\n", filePathLength, testCase.expected, spew.Sdump(changes)) + } + if normalized != testCase.expectedNormalized && !testCase.mustFail { + t.Fatalf("Normalization expected %v, got %v", testCase.expectedNormalized, normalized) + } + }) + } +} diff --git a/pkg/torrentStructures/torrent.go b/pkg/torrentStructures/torrent.go index 8c5abd8..4b2af7a 100644 --- a/pkg/torrentStructures/torrent.go +++ b/pkg/torrentStructures/torrent.go @@ -11,6 +11,7 @@ type Torrent struct { PieceLayers *map[string]interface{} `bencode:"piece layers"` FilePathLength *[]FilepathLength `bencode:"-"` // service field FilePaths *[]string `bencode:"-"` // service field + Single *bool `bencode:"-"` // service field } type TorrentInfo struct { diff --git a/test/data/tests_slahes_emoji_ut_created.torrent b/test/data/tests_slahes_emoji_ut_created.torrent new file mode 100644 index 0000000..95eab09 --- /dev/null +++ b/test/data/tests_slahes_emoji_ut_created.torrent @@ -0,0 +1 @@ +d8:announce44:udp://tracker.openbittorrent.com:80/announce13:announce-listll44:udp://tracker.openbittorrent.com:80/announceel42:udp://tracker.opentrackr.org:1337/announceee10:created by12:uTorrent/3.613:creation datei1701616527e8:encoding5:UTF-84:infod5:filesld6:lengthi8e4:pathl26:file_with_emoji_.txteed6:lengthi8e4:pathl19:file_with_slash.txteed6:lengthi8e4:pathl34:testdit_with_emoji_and_space26:file_with_emoji_.txteed6:lengthi8e4:pathl34:testdit_with_emoji_and_space19:file_with_slash.txteed6:lengthi8e4:pathl18:testdit_with_space26:file_with_emoji_.txteed6:lengthi8e4:pathl18:testdit_with_space19:file_with_slash.txteee4:name25:tests_slahes_emoji 12:piece lengthi16384e6:pieces20:tYlYd˝wUee \ No newline at end of file