Passing FILE to fopen

I have a C function cmark_node *cmark_parse_file(FILE *f, int options); to which I'd like to pass in a FILE handle. I currently have the following code

public func parse(file: URL, options: Options) throws -> Node? {
        var cFileHandle: UnsafeMutablePointer<FILE>?
        defer {
            if let cFile = cFileHandle {
                fclose(cFile)
            }
        }

        cFileHandle = try file.withUnsafeFileSystemRepresentation({ cFileUrl in
            if cFileUrl == nil {
                throw Errors.invalidFileUrl
            }

            return fopen(cFileUrl, "r")
        })

        if cFileHandle == nil {
            throw Errors.invalidFile
        }

        return cmark_parse_file(cFileHandle, options.rawValue)
            .map { Node(owned: $0) }
    }

Here, I'm trying to

  1. always close the file handle if opened
  2. Avoid passing a null pointer to fopen - if the URL cannot be represented by a file system representation

Is there a way to simplify this code? - primarily around this bit

 cFileHandle = try file.withUnsafeFileSystemRepresentation({ cFileUrl in
            if cFileUrl == nil {
                throw Errors.invalidFileUrl
            }

            return fopen(cFileUrl, "r")
        })

if cFileHandle == nil {
   throw Errors.invalidFile
}

For a one-off I’d write it like so:

func parse(url: URL) {
    guard let f = fopen(url.path, "r") else {
        … throw …
    }
    defer { fclose(f) }
    parse(file: f)
}

If I found myself doing this a lot, I’d write a wrapper that hides all the unsafe stuff.

extension URL {

    func withOpenCFile<Result>(_ body: (_ file: UnsafeMutablePointer<FILE>) -> Result) throws -> Result {
        guard let f = fopen(self.path, "r") else {
            … throw …
        }
        defer { fclose(f) }
        return body(f)
    }
}

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

4 Likes

Thank you. I didn't realize Swift would automatically bridge a String to C, and, we could add the defer after the guard. This is a lot simpler :)

Does url.path give the file system representation of the URL? I assume it would return an empty string in the event the URL doesn't really map to a file on the system?

Does url.path give the file system representation of the URL?

Yes.

This assumes that this is a file system URL. If you want to check for that, add the following:

guard self.isFileURL else {
    … something …
}

The only potential gotcha here is with platforms that don’t use UTF-8 as their file system representation. That’s not an issue on Apple platforms or Linux. I’m less confident about Windows.

I assume it would return an empty string in the event the URL doesn't
really map to a file on the system?

Nope.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

1 Like

I've found out (the hard way) that fopen(url.path, "r") fails (errno is "No such file or directory") if the URL contains space (that is being converted into %20 in a path). Meanwhile url.withUnsafeFileSystemRepresentation works flawlessly.

As I understand var path: String is deprecated (since when?) and we are supposed to use func path(percentEncoded:).

1 Like

Ha. That's a good catch. And that's my fault after all.
Most likely the compiler said path is deprecated and I've accepted the suggested change to the path() which passes true for its percentEncoded parameter. With such a path, the fopen fails if there is a space in the URL.
The fopen seems to be happy with the output of the now deprecated path or path(percentEncoded: false). Providing true for the path(percentEncoded:) (or simple path()) makes the fopen to fail.

As for deprecation - as the path(percentEncoded:) has been available since macOS 13, I guess that's the point when the path was marked as deprecated.

These changes to URL made me stop using it for file paths and instead switch to Swift System’s FilePath. That doesn't handle anything besides file paths (no HTTP, etc.), but it does give better access to the platform-specific representation, including file paths on Windows.

1 Like

Nice. That one is worth considering for switching to it.

Correct, Windows requires UTF-16 for the file path strings. Additionally, .path will not give the FSR, which is important for Windows API calls. You should use withUnsafeFileSystemRepresentation and then re-code the string to UTF-16 for maximal safety on Windows.

florianpircher wrote:

These changes to URL made me stop using it for file paths and
instead switch to Swift System’s FilePath.

There is a downside to that though. On Apple platforms a URL can carry a security scope and a FilePath can’t. Consider this example:

  1. You’re in a sandboxed app and you call NSOpenPanel.

  2. It gives you back a security-scoped URL.

  3. You convert it to a FilePath as a matter of routine.

  4. Later on you then try to access it. That requires you to convert the path back to a URL because you have to call startAccessingSecurityScopedResource(). That conversion works.

  5. But then startAccessingSecurityScopedResource() fails because you lost the security scope in step 3.


compnerd wrote:

You should use withUnsafeFileSystemRepresentation

This is like déjà vu all over again (-:

It’s before my time, but my understanding is that Foundation added the ‘file system representation’ stuff to support Windows. After Yellow Box for Windows died, we all went back to just passing in strings. But now we have to be careful again!

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

4 Likes

Thanks for pointing it out! It does not apply to my current use case, but it might be relevant in the future. I wrapped FilePath anyway as AbsoluteFilePath and RelativeFilePath, so I should be able to swap the underlying storage for a URL if the need arises.

1 Like