URL.checkResourceIsReachable() throws error if file is on FTP server and name contains special characters

I have a file named ä.txt (with German umlaut) on my FTP server. I select it like this:

let openPanel = NSOpenPanel()
openPanel.runModal()
let source = openPanel.urls[0]

Running this code unexpectedly throws an error:

do {
    print(try source.checkResourceIsReachable())
} catch {
    print(error) // Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory”
}

Manipulating the URL also seems to change the underlying characters:

print(source)                            // file:///Volumes/abc.com/httpdocs/%C3%A4.txt
print(URL(fileURLWithPath: source.path)) // file:///Volumes/abc.com/httpdocs/a%CC%88.txt

Note that both variants of the URL above also throw the same error when running URL.checkResourceIsReachable().

If I download the file to my Mac, then both variants print file:///Users/me/Downloads/a%CC%88.txt and neither of them throws an error when running URL.checkResourceIsReachable().

What is the problem? How can I correctly access this file on the FTP server?

Replies

Have you tried encoding the file name and creating url from it? something like

let urlEncoded = value.addingPercentEncoding(withAllowedCharacters: .alphanumerics)
let url = "http://www.myftpserver.com/?name=\(urlEncoded!)"

Have you tried encoding the file name and creating url from it?

No, since my app relies on the Finder to mount and connect to FTP volumes, so I can only use file URLs.

After a few more tests, I noticed that, while try source.checkResourceIsReachable() throws an error and FileManager.default.fileExists(atPath: source.path) returns false, calling open(source.path, O_RDONLY) returns a valid file descriptor if source is a directory; if it's a regular file, it returns -1.

FTP is fundamentally broken at the protocol level. You won’t be able to make non-ASCII file names work with FTP in the general case. I talk about FTP’s brokenness in detail in On FTP.

The issue you’re hitting is almost certainly related to normalisation. Back when I spent more time on file system stuff I wrote a couple of Q&As about this:

Sadly, while these are useful background, the specific advice they contain in quite naïve. These days I’m not convinced there is a good solution to this problem.

Foundation has a tendency — it’s debatable as to whether it’s a good or a bad thing — to convert file system path strings to their decomposed form. That behaviour originated in the days of HFS Plus, where the file system decomposed all file names.

Consider this small program:

import Foundation

func main() {
    let u1 = URL(fileURLWithPath: "/na\u{00ef}ve")
    let u2 = URL(fileURLWithPath: "/nai\u{0308}ve")
    let u3 = URL(string: "file:///na\u{00ef}ve")!
    let u4 = URL(string: "file:///nai\u{0308}ve")!

    print((Data(u1.path.utf8) as NSData).debugDescription)
    print((Data(u2.path.utf8) as NSData).debugDescription)
    print((Data(u4.path.utf8) as NSData).debugDescription)
    print((Data(u3.path.utf8) as NSData).debugDescription)
}

main()

It prints:

<2f6e6169 cc887665>
<2f6e6169 cc887665>
<2f6e6169 cc887665>
<2f6e61c3 af7665>

Note that, when using init(fileURLWithPath:), both the pre- and decomposed paths result in a decomposed path.

IIRC the FTP VFS plug-in is normalisation sensitive, so it finds the file if you give it the precomposed name but not if you give it the decomposed one.

As to how you get around this… sheesh… if I were in your shoes I just wouldn’t. There’s no point investing time in FTP. Moreover, this isn’t just about your time now: Any fix is going to complicate your ongoing maintenance.

If you really want to spend the time, it’s still tricky to fix. The problem is that the name might contain both pre- and decomposed sequences, so you can’t just convert the string to precomposed and retry with that. You’d have to iterate the parent directory and look through the results with a normalisation-insensitive match. And there are two problems with that:

  • The path to the directory might contain precomposed sequences, so you might need to recursively apply this process up the tree.

  • There might be multiple matches. A normalisation-insensitive file system can contain both na\u{00ef}ve and nai\u{0308}ve. If you find multiple matches, which one is correct?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

FTP is fundamentally broken at the protocol level. You won’t be able to make non-ASCII file names work with FTP in the general case.

Thanks for your comments.

Regarding your link "On FTP", I was wondering about this:

FTPS is FTP over TLS (aka SSL). While FTPS adds security to the protocol, which is very important, it still inherits many of FTP’s other problems. Personally I try to avoid this protocol.

in particular the part "it still inherits many of FTP’s other problems". What are these other problems? At the beginning of the post you only mentioned privacy and security, but these are already fixed with FTPS according to you.

Also do you have any clue why open(source.path, O_RDONLY) returns a valid file descriptor if source is a directory and if it's a regular file it returns -1 (see my previous post)?

What are these other problems? At the beginning of the post you only mentioned privacy and security

The post mentions the inability “to create a GUI client that reliably shows a directory listing in a platform-independent manner”. I’ve added a footnote to explain that in more detail.

Also do you have any clue why … ?

Nope, sorry.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"