diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Interface/NextcloudKit+RemoteInterface.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Interface/NextcloudKit+RemoteInterface.swift index 44923a103598e..4ee4584c2c264 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Interface/NextcloudKit+RemoteInterface.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Interface/NextcloudKit+RemoteInterface.swift @@ -7,6 +7,22 @@ import Foundation import NextcloudCapabilitiesKit import NextcloudKit +func chunkedUploadRemotePathComponents(from remotePath: String) -> (serverUrl: String, destinationFileName: String)? { + guard let separatorIndex = remotePath.lastIndex(of: "/") else { + return nil + } + + let destinationFileNameIndex = remotePath.index(after: separatorIndex) + guard destinationFileNameIndex < remotePath.endIndex else { + return nil + } + + return ( + serverUrl: String(remotePath[.. (account: String, file: NKFile?, nkError: NKError) { let logger = FileProviderLogger(category: "NextcloudKit+RemoteInterface", log: log) - guard let remoteUrl = URL(string: remotePath) else { + guard let remotePathComponents = chunkedUploadRemotePathComponents(from: remotePath) else { return ("", nil, .urlError) } let localUrl = URL(fileURLWithPath: localPath) @@ -136,17 +152,8 @@ extension NextcloudKit: RemoteInterface { } let fileChunksOutputDirectory = chunksOutputDirectoryUrl.path let fileName = localUrl.lastPathComponent - let destinationFileName = remoteUrl.lastPathComponent - guard let serverUrl = remoteUrl - .deletingLastPathComponent() - .absoluteString - .removingPercentEncoding - else { - logger.error( - "NCKit ext: Could not get server url from \(remotePath)" - ) - return ("", nil, .urlError) - } + let destinationFileName = remotePathComponents.destinationFileName + let serverUrl = remotePathComponents.serverUrl let fileChunks = remainingChunks.toNcKitChunks() logger.info( diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/RemoteInterfaceTests.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/RemoteInterfaceTests.swift index 55cd11f9bf4b4..4e773a1d68287 100644 --- a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/RemoteInterfaceTests.swift +++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/RemoteInterfaceTests.swift @@ -15,6 +15,27 @@ struct RemoteInterfaceExtensionTests { let testAccount = Account(user: "a1", id: "1", serverUrl: "example.com", password: "pass") let otherAccount = Account(user: "a2", id: "2", serverUrl: "example.com", password: "word") + @Test func chunkedUploadRemotePathComponentsPreserveFilenameCharacters() throws { + let serverUrl = "https://cloud.example.com/remote.php/dav/files/user/comics" + let fileNames = [ + "The Nightly News #001 (2011).cbz", + "Question?.txt", + "Literal%23Name.txt" + ] + + for fileName in fileNames { + let components = try #require(chunkedUploadRemotePathComponents(from: "\(serverUrl)/\(fileName)")) + + #expect(components.serverUrl == serverUrl) + #expect(components.destinationFileName == fileName) + } + } + + @Test func chunkedUploadRemotePathComponentsRejectInvalidPaths() { + #expect(chunkedUploadRemotePathComponents(from: "filename.txt") == nil) + #expect(chunkedUploadRemotePathComponents(from: "https://cloud.example.com/") == nil) + } + func capabilitiesFromMockJSON(jsonString: String = mockCapabilities) -> (Capabilities, Data) { let data = jsonString.data(using: .utf8)! let caps = Capabilities(data: data)!