From fda659db74fa0b719ef5b2298c70209486954835 Mon Sep 17 00:00:00 2001 From: Alexey Kostin Date: Sat, 25 Nov 2023 22:47:40 +0300 Subject: [PATCH] Cesu8 (#44) * cesu8 handler * resume file cesu8 handle * utf8 emoji filepaths * cesu8 emoji filepaths and torrent names * cesu8 emoji filepaths and torrent names handling improve tests * add fastresume Name field * update golang ver * handle torrent files with cesu8 encoded file paths and handle them as NoSubfolder torrents for better compatibility * fix cesu8 named single file torrents --- Makefile | 2 +- go.mod | 2 +- go.sum | 4 +- internal/transfer/fastresumeHandle.go | 8 +- internal/transfer/resumeHandle.go | 2 +- internal/transfer/resumeHandle_test.go | 32 +++ internal/transfer/transfer.go | 56 ++-- internal/transfer/transfer_test.go | 269 +++++++++++++++++- pkg/helpers/helpers.go | 8 + pkg/helpers/helpers_test.go | 7 + pkg/qBittorrentStructures/qBittorrent.go | 1 + .../normal_text πŸ†• normal_text 🚜.txt.torrent | 1 + test/data/resume_emoji_clear.dat | 1 + 13 files changed, 361 insertions(+), 32 deletions(-) create mode 100644 test/data/normal_text πŸ†• normal_text 🚜.txt.torrent create mode 100644 test/data/resume_emoji_clear.dat diff --git a/Makefile b/Makefile index 66e9293..1a509aa 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -gotag=1.18.1-bullseye +gotag=1.21.4-bullseye commit=$(shell git rev-parse HEAD) diff --git a/go.mod b/go.mod index cc3675e..ba92912 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/rumanzo/bt2qbt go 1.16 require ( + github.com/crazytyper/go-cesu8 v0.0.0-20190615112902-270517b5a01c github.com/davecgh/go-spew v1.1.0 github.com/fatih/color v1.13.0 - github.com/go-ini/ini v1.64.0 github.com/jessevdk/go-flags v1.5.0 github.com/r3labs/diff/v2 v2.15.0 github.com/stretchr/testify v1.7.0 // indirect diff --git a/go.sum b/go.sum index c1febcb..c22f2a9 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ +github.com/crazytyper/go-cesu8 v0.0.0-20190615112902-270517b5a01c h1:LslEy3hCBNp2TfgmcmJBIIAprB51yH+AIBC3kVDxlGc= +github.com/crazytyper/go-cesu8 v0.0.0-20190615112902-270517b5a01c/go.mod h1:eWhedTyAcrUdtMYyEjm6HmjjwSGRre54xWeBBZGhhYc= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/go-ini/ini v1.64.0 h1:73w/ADE+yoYjfu4BlI/LaEMe9Do1zOQ6qPt1du4uikI= -github.com/go-ini/ini v1.64.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= diff --git a/internal/transfer/fastresumeHandle.go b/internal/transfer/fastresumeHandle.go index 199fd30..04a45c7 100644 --- a/internal/transfer/fastresumeHandle.go +++ b/internal/transfer/fastresumeHandle.go @@ -1,6 +1,7 @@ package transfer import ( + "github.com/rumanzo/bt2qbt/pkg/helpers" "time" ) @@ -8,12 +9,17 @@ func (transfer *TransferStructure) HandleStructures() { if ok := transfer.ResumeItem.Targets; ok != nil { for _, entry := range transfer.ResumeItem.Targets { - transfer.Targets[entry[0].(int64)] = entry[1].(string) + transfer.Targets[entry[0].(int64)] = helpers.HandleCesu8(entry[1].(string)) } } // 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 diff --git a/internal/transfer/resumeHandle.go b/internal/transfer/resumeHandle.go index 58f1eb5..a6c1fa8 100644 --- a/internal/transfer/resumeHandle.go +++ b/internal/transfer/resumeHandle.go @@ -107,7 +107,7 @@ func HandleResumeItems(opts *options.Opts, resumeItems map[string]*utorrentStruc transferStruct.ResumeItem = resumeItem transferStruct.Replace = replaces transferStruct.Opts = opts - go HandleResumeItem(key, &transferStruct, &chans, &wg) + go HandleResumeItem(helpers.HandleCesu8(key), &transferStruct, &chans, &wg) } go func() { wg.Wait() diff --git a/internal/transfer/resumeHandle_test.go b/internal/transfer/resumeHandle_test.go index 608df95..a5bb737 100644 --- a/internal/transfer/resumeHandle_test.go +++ b/internal/transfer/resumeHandle_test.go @@ -4,7 +4,9 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/r3labs/diff/v2" "github.com/rumanzo/bt2qbt/internal/options" + "github.com/rumanzo/bt2qbt/pkg/fileHelpers" "github.com/rumanzo/bt2qbt/pkg/helpers" + "github.com/rumanzo/bt2qbt/pkg/torrentStructures" "reflect" "testing" ) @@ -134,6 +136,16 @@ func TestHandleTorrentFilePath(t *testing.T) { Opts: &options.Opts{BitDir: `C:\\temp`}, }, }, + { + name: "007 Check emoji", + key: "normal_text \xf0\x9f\x86\x95 normal_text \xf0\x9f\x9a\x9c.txt.torrent", + newTransferStructure: &TransferStructure{Opts: &options.Opts{BitDir: `C:\\temp`}}, + expected: &TransferStructure{ + TorrentFilePath: "C:/temp/normal_text \xf0\x9f\x86\x95 normal_text \xf0\x9f\x9a\x9c.txt.torrent", + TorrentFileName: "normal_text \xf0\x9f\x86\x95 normal_text \xf0\x9f\x9a\x9c.txt.torrent", + Opts: &options.Opts{BitDir: `C:\\temp`}, + }, + }, } for _, testCase := range cases { @@ -160,3 +172,23 @@ func TestPath(t *testing.T) { t.Fatalf("Can't decode torrent file with error: %v", err) } } + +func TestEmojiResumeDecode(t *testing.T) { + resumeFilePath := "../../test/data/resume_emoji_clear.dat" + torrentsDir := "../../test/data/" + resumeFile := map[string]interface{}{} + err := helpers.DecodeTorrentFile(resumeFilePath, resumeFile) + if err != nil { + t.Fatalf("Can't decode torrent file with error: %v", err) + } + for k, _ := range resumeFile { + torrentFile := torrentStructures.Torrent{} + err := helpers.DecodeTorrentFile(fileHelpers.Join([]string{torrentsDir, helpers.HandleCesu8(k)}, "/"), &torrentFile) + if err != nil { + t.Fatalf("Can't decode torrent file with error: %v", err) + } + if torrentFile.CreationDate == 0 { + t.Fatal("Decoded values is wrong") + } + } +} diff --git a/internal/transfer/transfer.go b/internal/transfer/transfer.go index e15538f..a578239 100644 --- a/internal/transfer/transfer.go +++ b/internal/transfer/transfer.go @@ -15,6 +15,7 @@ import ( "regexp" "strings" "time" + "unicode/utf8" ) //goland:noinspection GoNameStartsWithPackageName @@ -76,7 +77,7 @@ func CreateEmptyNewTransferStructure() TransferStructure { func (transfer *TransferStructure) HandleCaption() { if transfer.ResumeItem.Caption != "" { - transfer.Fastresume.QbtName = transfer.ResumeItem.Caption + transfer.Fastresume.QbtName = helpers.HandleCesu8(transfer.ResumeItem.Caption) } } @@ -127,7 +128,7 @@ func (transfer *TransferStructure) HandleTags() { if transfer.Opts.WithoutTags == false && transfer.ResumeItem.Labels != nil { for _, label := range transfer.ResumeItem.Labels { if label != "" { - transfer.Fastresume.QbtTags = append(transfer.Fastresume.QbtTags, label) + transfer.Fastresume.QbtTags = append(transfer.Fastresume.QbtTags, helpers.HandleCesu8(label)) } } } else { @@ -136,7 +137,7 @@ func (transfer *TransferStructure) HandleTags() { } func (transfer *TransferStructure) HandleLabels() { if transfer.Opts.WithoutLabels == false { - transfer.Fastresume.QBtCategory = transfer.ResumeItem.Label + transfer.Fastresume.QBtCategory = helpers.HandleCesu8(transfer.ResumeItem.Label) } else { transfer.Fastresume.QBtCategory = "" } @@ -155,9 +156,9 @@ func (transfer *TransferStructure) HandleTrackers() { index = "main" } if _, ok := trackersMap[index]; ok { - trackersMap[index] = append(trackersMap[index], tracker) + trackersMap[index] = append(trackersMap[index], helpers.HandleCesu8(tracker)) } else { - trackersMap[index] = []string{tracker} + trackersMap[index] = []string{helpers.HandleCesu8(tracker)} } } if val, ok := trackersMap["main"]; ok { @@ -268,40 +269,51 @@ func (transfer *TransferStructure) HandleSavePaths() { // qBtSavePath always has separator /, otherwise SavePath use os pathSeparator if transfer.Magnet { transfer.Fastresume.QBtContentLayout = "Original" - transfer.Fastresume.QbtSavePath = fileHelpers.Normalize(transfer.ResumeItem.Path, "/") + transfer.Fastresume.QbtSavePath = fileHelpers.Normalize(helpers.HandleCesu8(transfer.ResumeItem.Path), "/") } else { var torrentName string + var torrentNameOriginal string if transfer.TorrentFile.Info.NameUTF8 != "" { - torrentName = transfer.TorrentFile.Info.NameUTF8 + torrentName = helpers.HandleCesu8(transfer.TorrentFile.Info.NameUTF8) + torrentNameOriginal = transfer.TorrentFile.Info.NameUTF8 } else { - torrentName = transfer.TorrentFile.Info.Name + torrentName = helpers.HandleCesu8(transfer.TorrentFile.Info.Name) + torrentNameOriginal = transfer.TorrentFile.Info.Name } - lastPathName := fileHelpers.Base(transfer.ResumeItem.Path) + lastPathName := fileHelpers.Base(helpers.HandleCesu8(transfer.ResumeItem.Path)) if len(transfer.TorrentFile.GetFileList()) > 0 { - if lastPathName == torrentName { + 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 { transfer.Fastresume.QBtContentLayout = "Original" - transfer.Fastresume.QbtSavePath = fileHelpers.CutLastPath(transfer.ResumeItem.Path, transfer.Opts.PathSeparator) + transfer.Fastresume.QbtSavePath = fileHelpers.CutLastPath(helpers.HandleCesu8(transfer.ResumeItem.Path), transfer.Opts.PathSeparator) if maxIndex := transfer.FindHighestIndexOfMappedFiles(); maxIndex >= 0 { transfer.Fastresume.MappedFiles = make([]string, maxIndex+1, maxIndex+1) for _, paths := range transfer.ResumeItem.Targets { index := paths[0].(int64) var pathParts []string - if fileHelpers.IsAbs(paths[1].(string)) { - pathParts = []string{fileHelpers.Normalize(paths[1].(string), transfer.Opts.PathSeparator)} + if fileHelpers.IsAbs(helpers.HandleCesu8(paths[1].(string))) { + pathParts = []string{fileHelpers.Normalize(helpers.HandleCesu8(paths[1].(string)), transfer.Opts.PathSeparator)} // if path is absolute just normalize it transfer.Fastresume.MappedFiles[index] = fileHelpers.Join(pathParts, transfer.Opts.PathSeparator) } else { pathParts = make([]string, len(paths)-1, len(paths)-1) for num, part := range paths[1:] { - pathParts[num] = part.(string) + 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.QbtSavePath = fileHelpers.CutLastPath(transfer.ResumeItem.Path, "/") + transfer.Fastresume.QbtSavePath = fileHelpers.CutLastPath(helpers.HandleCesu8(transfer.ResumeItem.Path), "/") if string(transfer.Fastresume.QbtSavePath[len(transfer.Fastresume.QbtSavePath)-1]) != `/` { transfer.Fastresume.QbtSavePath += `/` } @@ -310,33 +322,33 @@ func (transfer *TransferStructure) HandleSavePaths() { // 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(filePath, transfer.Opts.PathSeparator)) + transfer.Fastresume.MappedFiles = append(transfer.Fastresume.MappedFiles, fileHelpers.Normalize(helpers.HandleCesu8(filePath), transfer.Opts.PathSeparator)) } // and then doing remap if resumeItem contain target field if maxIndex := transfer.FindHighestIndexOfMappedFiles(); maxIndex >= 0 { for _, paths := range transfer.ResumeItem.Targets { index := paths[0].(int64) var pathParts []string - if fileHelpers.IsAbs(paths[1].(string)) { - pathParts = []string{fileHelpers.Normalize(paths[1].(string), transfer.Opts.PathSeparator)} + if fileHelpers.IsAbs(helpers.HandleCesu8(paths[1].(string))) { + pathParts = []string{fileHelpers.Normalize(helpers.HandleCesu8(paths[1].(string)), transfer.Opts.PathSeparator)} } else { pathParts = make([]string, len(paths)-1, len(paths)-1) for num, part := range paths[1:] { - pathParts[num] = part.(string) + pathParts[num] = helpers.HandleCesu8(part.(string)) } } transfer.Fastresume.MappedFiles[index] = fileHelpers.Join(pathParts, transfer.Opts.PathSeparator) } } - transfer.Fastresume.QbtSavePath = fileHelpers.Normalize(transfer.ResumeItem.Path, "/") + transfer.Fastresume.QbtSavePath = fileHelpers.Normalize(helpers.HandleCesu8(transfer.ResumeItem.Path), "/") } } else { transfer.Fastresume.QBtContentLayout = "Original" // utorrent\bittorrent don't support create subfolders for torrents with single file - if lastPathName != torrentName { + if lastPathName != torrentNameOriginal { //it means that we have renamed path and targets item, and should have mapped files transfer.Fastresume.MappedFiles = []string{lastPathName} } - transfer.Fastresume.QbtSavePath = fileHelpers.CutLastPath(transfer.ResumeItem.Path, `/`) + transfer.Fastresume.QbtSavePath = fileHelpers.CutLastPath(helpers.HandleCesu8(transfer.ResumeItem.Path), `/`) if string(transfer.Fastresume.QbtSavePath[len(transfer.Fastresume.QbtSavePath)-1]) != `/` { transfer.Fastresume.QbtSavePath += `/` } diff --git a/internal/transfer/transfer_test.go b/internal/transfer/transfer_test.go index 48adbbf..6e005f3 100644 --- a/internal/transfer/transfer_test.go +++ b/internal/transfer/transfer_test.go @@ -659,7 +659,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { }, }, { - name: "021 Test torrent with windows folder (Original) path without replaces. Moved files with absolute paths. Windows share", + name: "021 Test torrent with windows share folder (Original) path without replaces. Moved files with absolute paths. Windows share", newTransferStructure: &TransferStructure{ Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, ResumeItem: &utorrentStructs.ResumeItem{ @@ -709,7 +709,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { }, }, { - name: "022 Test torrent with windows folder (NoSubfolder) path without replaces. Moved files with absolute paths", + name: "022 Test torrent with windows share folder (NoSubfolder) path without replaces. Moved files with absolute paths", newTransferStructure: &TransferStructure{ Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, ResumeItem: &utorrentStructs.ResumeItem{ @@ -902,7 +902,7 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { }, }, { - name: "027 Test torrent with signle file torrent and savepath in rootdirectory", + name: "027 Test torrent with multi file torrent and savepath in rootdirectory", newTransferStructure: &TransferStructure{ Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, ResumeItem: &utorrentStructs.ResumeItem{ @@ -930,6 +930,267 @@ func TestTransferStructure_HandleSavePaths(t *testing.T) { }, }, }, + { + name: "028 Test torrent with windows folder (original) path without replaces. Emoji utf8 in file and name", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{ + Path: "D:\\torrents\\test_torrent \xf0\x9f\x86\x95", + Targets: [][]interface{}{ + []interface{}{ + int64(0), + "E:\\somedir1 \xf0\x9f\x86\x95\\\xf0\x9f\x86\x95 renamed_test_torrent2.txt", + }, + []interface{}{ + int64(1), + "\\\\somedir\\somedir4 \xf0\x9f\x86\x95\\\xf0\x9f\x86\x95 renamed_test_torrent3.txt", + }, + []interface{}{ + int64(2), + "renamed \xf0\x9f\x86\x95 file1.txt", + }, + }, + }, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_torrent \xf0\x9f\x86\x95", + Files: []*torrentStructures.TorrentFile{ + &torrentStructures.TorrentFile{Path: []string{"dir1", "\xf0\x9f\x86\x95 file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"\xf0\x9f\x86\x95", "file2.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file0 \xf0\x9f\x86\x95.txt"}}, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: `D:/torrents/`, + SavePath: `D:\torrents\`, + QBtContentLayout: "Original", + MappedFiles: []string{ + "E:\\somedir1 \xf0\x9f\x86\x95\\\xf0\x9f\x86\x95 renamed_test_torrent2.txt", + "\\\\somedir\\somedir4 \xf0\x9f\x86\x95\\\xf0\x9f\x86\x95 renamed_test_torrent3.txt", + "test_torrent \xf0\x9f\x86\x95\\renamed \xf0\x9f\x86\x95 file1.txt", + }, + }, + }, + }, + { + name: "029 Test torrent with windows folder (NoSubfolder) with renamed files. Emoji utf8 in file and torrent name", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{Path: "D:\\torrents\\renamed test_torrent \xf0\x9f\x86\x95"}, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_torrent \xf0\x9f\x86\x95", + Files: []*torrentStructures.TorrentFile{ + &torrentStructures.TorrentFile{Path: []string{"dir1", "\xf0\x9f\x86\x95 file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"\xf0\x9f\x86\x95", "file2.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file0 \xf0\x9f\x86\x95.txt"}}, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: "D:/torrents/renamed test_torrent \xf0\x9f\x86\x95", + SavePath: "D:\\torrents\\renamed test_torrent \xf0\x9f\x86\x95", + QBtContentLayout: "NoSubfolder", + MappedFiles: []string{ + "dir1\\\xf0\x9f\x86\x95 file1.txt", + "\xf0\x9f\x86\x95\\file2.txt", + "file0 \xf0\x9f\x86\x95.txt", + }, + }, + }, + }, + { + name: "030 Test torrent with windows folder (original) path with renamed files. Emoji cesu8 in file and name", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{ + Path: "D:\\torrents\\test_torrent \xed\xa0\xbc\xed\xb6\x95", + Targets: [][]interface{}{ + []interface{}{ + int64(0), + "E:\\somedir1 \xed\xa0\xbc\xed\xb6\x95\\\xed\xa0\xbc\xed\xb6\x95 renamed_test_torrent2.txt", + }, + []interface{}{ + int64(1), + "\\\\somedir\\somedir4 \xed\xa0\xbc\xed\xb6\x95\\\xed\xa0\xbc\xed\xb6\x95 renamed_test_torrent3.txt", + }, + []interface{}{ + int64(2), + "renamed \xed\xa0\xbc\xed\xb6\x95 file1.txt", + }, + }, + }, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_torrent \xed\xa0\xbc\xed\xb6\x95", + Files: []*torrentStructures.TorrentFile{ + &torrentStructures.TorrentFile{Path: []string{"dir1", "\xed\xa0\xbc\xed\xb6\x95 file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"\xed\xa0\xbc\xed\xb6\x95", "file2.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file0 \xed\xa0\xbc\xed\xb6\x95.txt"}}, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: "D:/torrents/test_torrent \xf0\x9f\x86\x95", + SavePath: "D:\\torrents\\test_torrent \xf0\x9f\x86\x95", + QBtContentLayout: "NoSubfolder", + MappedFiles: []string{ + "E:\\somedir1 \xf0\x9f\x86\x95\\\xf0\x9f\x86\x95 renamed_test_torrent2.txt", + "\\\\somedir\\somedir4 \xf0\x9f\x86\x95\\\xf0\x9f\x86\x95 renamed_test_torrent3.txt", + "renamed \xf0\x9f\x86\x95 file1.txt", + }, + }, + }, + }, + { + name: "031 Test torrent with windows folder (NoSubfolder) with renamed files. Emoji cesu8 in file and torrent name", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{Path: "D:\\torrents\\renamed test_torrent \xed\xa0\xbc\xed\xb6\x95"}, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_torrent \xed\xa0\xbc\xed\xb6\x95", + Files: []*torrentStructures.TorrentFile{ + &torrentStructures.TorrentFile{Path: []string{"dir1", "\xed\xa0\xbc\xed\xb6\x95 file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"\xed\xa0\xbc\xed\xb6\x95", "file2.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file0 \xed\xa0\xbc\xed\xb6\x95.txt"}}, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: "D:/torrents/renamed test_torrent \xf0\x9f\x86\x95", + SavePath: "D:\\torrents\\renamed test_torrent \xf0\x9f\x86\x95", + QBtContentLayout: "NoSubfolder", + MappedFiles: []string{ + "dir1\\\xf0\x9f\x86\x95 file1.txt", + "\xf0\x9f\x86\x95\\file2.txt", + "file0 \xf0\x9f\x86\x95.txt", + }, + }, + }, + }, + { + name: "032 Test torrent with windows folder without renamed files. Emoji cesu8 in file and torrent name", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{Path: "D:\\torrents\\test_torrent"}, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_torrent", + Files: []*torrentStructures.TorrentFile{ + &torrentStructures.TorrentFile{Path: []string{"dir1", "\xed\xa0\xbc\xed\xb6\x95 file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"\xed\xa0\xbc\xed\xb6\x95", "file2.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file0 \xed\xa0\xbc\xed\xb6\x95.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file2 \xed\xa0\xbc\xed\xb6\x95.txt"}}, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: "D:/torrents/test_torrent", + SavePath: "D:\\torrents\\test_torrent", + QBtContentLayout: "NoSubfolder", + MappedFiles: []string{ + "dir1\\\xf0\x9f\x86\x95 file1.txt", + "\xf0\x9f\x86\x95\\file2.txt", + "file0 \xf0\x9f\x86\x95.txt", + `file1.txt`, + "file2 \xf0\x9f\x86\x95.txt", + }, + }, + }, + }, + { + name: "033 Test torrent with windows folder with renamed files. Emoji cesu8 in file and torrent name", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{ + Path: "D:\\torrents\\test_torrent", + Targets: [][]interface{}{ + []interface{}{ + int64(0), + "E:\\somedir1 \xed\xa0\xbc\xed\xb6\x95\\\xed\xa0\xbc\xed\xb6\x95 renamed_test_torrent2.txt", + }, + []interface{}{ + int64(1), + "\\\\somedir\\somedir4 \xed\xa0\xbc\xed\xb6\x95\\\xed\xa0\xbc\xed\xb6\x95 renamed_test_torrent3.txt", + }, + []interface{}{ + int64(3), + "renamed \xed\xa0\xbc\xed\xb6\x95 file1.txt", + }, + }, + }, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_torrent", + Files: []*torrentStructures.TorrentFile{ + &torrentStructures.TorrentFile{Path: []string{"dir1", "\xed\xa0\xbc\xed\xb6\x95 file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"\xed\xa0\xbc\xed\xb6\x95", "file2.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file0 \xed\xa0\xbc\xed\xb6\x95.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file1.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file2 \xed\xa0\xbc\xed\xb6\x95.txt"}}, + &torrentStructures.TorrentFile{Path: []string{"file4.txt"}}, + }, + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: "D:/torrents/test_torrent", + SavePath: "D:\\torrents\\test_torrent", + QBtContentLayout: "NoSubfolder", + MappedFiles: []string{ + "E:\\somedir1 \xf0\x9f\x86\x95\\\xf0\x9f\x86\x95 renamed_test_torrent2.txt", + "\\\\somedir\\somedir4 \xf0\x9f\x86\x95\\\xf0\x9f\x86\x95 renamed_test_torrent3.txt", + "file0 \xf0\x9f\x86\x95.txt", + "renamed \xf0\x9f\x86\x95 file1.txt", + "file2 \xf0\x9f\x86\x95.txt", + "file4.txt", + }, + }, + }, + }, + { + name: "034 Test torrent with windows single nofolder (original) path without replaces. Cesu8 emoji", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + ResumeItem: &utorrentStructs.ResumeItem{Path: "D:\\torrents\\test_torrent \xed\xa0\xbc\xed\xb6\x95.txt"}, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + Name: "test_torrent \xed\xa0\xbc\xed\xb6\x95.txt", + }, + }, + Opts: &options.Opts{PathSeparator: `\`}, + }, + expected: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + QbtSavePath: `D:/torrents/`, + SavePath: `D:\torrents\`, + MappedFiles: []string{ + "test_torrent \xf0\x9f\x86\x95.txt", + }, + QBtContentLayout: "Original", + }, + }, + }, } for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { @@ -945,7 +1206,7 @@ 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:\n Got: %#v \n Expect %#v \n Diff: %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)) } diff --git a/pkg/helpers/helpers.go b/pkg/helpers/helpers.go index 87d47bd..44bee4d 100644 --- a/pkg/helpers/helpers.go +++ b/pkg/helpers/helpers.go @@ -3,6 +3,7 @@ package helpers import ( "bufio" "bytes" + "github.com/crazytyper/go-cesu8" "github.com/zeebo/bencode" "io" "io/ioutil" @@ -111,3 +112,10 @@ func GetStrings(trackers interface{}) []string { } return ntrackers } + +func HandleCesu8(str string) string { + if strings.Contains(str, "\xed\xa0") { + return cesu8.DecodeString([]byte(str)) + } + return str +} diff --git a/pkg/helpers/helpers_test.go b/pkg/helpers/helpers_test.go index 24f6fea..9a8dca3 100644 --- a/pkg/helpers/helpers_test.go +++ b/pkg/helpers/helpers_test.go @@ -57,3 +57,10 @@ 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" + if utf8 != HandleCesu8(cesu8) { + t.Fatalf("Cesu8 to utf-8 transformation fail") + } +} diff --git a/pkg/qBittorrentStructures/qBittorrent.go b/pkg/qBittorrentStructures/qBittorrent.go index 759101a..363547c 100644 --- a/pkg/qBittorrentStructures/qBittorrent.go +++ b/pkg/qBittorrentStructures/qBittorrent.go @@ -30,6 +30,7 @@ type QBittorrentFastresume struct { MappedFiles []string `bencode:"mapped_files,omitempty"` // list of strings. If any file in the torrent has been renamed, this entry contains a list of all the filenames. In the same order as in the torrent file. MaxConnections int64 `bencode:"max_connections"` // integer. The max number of peer connections this torrent may have, if a limit is set. MaxUploads int64 `bencode:"max_uploads"` // integer. The max number of unchoked peers this torrent may have, if a limit is set. + Name string `bencode:"name"` NumComplete int64 `bencode:"num_complete"` NumDownloaded int64 `bencode:"num_downloaded"` NumIncomplete int64 `bencode:"num_incomplete"` diff --git a/test/data/normal_text πŸ†• normal_text 🚜.txt.torrent b/test/data/normal_text πŸ†• normal_text 🚜.txt.torrent new file mode 100644 index 0000000..9fbb8c1 --- /dev/null +++ b/test/data/normal_text πŸ†• normal_text 🚜.txt.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 datei1700849684e8:encoding5:UTF-84:infod6:lengthi4e4:name41:normal_text ν ΌνΆ• normal_text 🚜.txt12:piece lengthi16384e6:pieces20:©JεΜ±›¦LsΣ‘ι‡˜/»Σee \ No newline at end of file diff --git a/test/data/resume_emoji_clear.dat b/test/data/resume_emoji_clear.dat new file mode 100644 index 0000000..0f85b35 --- /dev/null +++ b/test/data/resume_emoji_clear.dat @@ -0,0 +1 @@ +d49:normal_text ν ΌνΆ• normal_text 🚜.txt.torrentd4:test4:testee \ No newline at end of file