`String.write()` not respecting Unix permissions?

I'm trying to use String.write(to:<fileURL>, ...), since it's simple and I'm fairly new to Swift.

Problem: this method overrides Unix write permission -- that is, the lack of write permission.

Here's a transcript showing a short program and what it does. I'm on a Mac mini (late 2018) running macOS 10.15.7 (Catalina).

The exciting bits are at the end, where ls -l shows the file (out.nowritepermission) still doesn't have write permission, while cat out.nowritepermission shows that its contents have changed.

Am I missing something?

$ swiftc --version
Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28)
Target: x86_64-apple-darwin19.6.0
$ cat permtest.swift
import Foundation
`let fileName = "out.nowritepermission"` `let dirPath = FileManager.default.currentDirectoryPath` `let filePath = NSString(string: dirPath).appendingPathComponent(fileName)` `let url = URL(fileURLWithPath: filePath)`
// Are we allowed?
let writeOK = FileManager.default.isWritableFile(atPath: filePath)
print("Are we allowed to write this file? -- \(writeOK ? "yes" : "no")")
``
// Try anyway.
let str = "This should not appear.\n"
try! str.write(to: url, atomically: true, encoding: .utf8)
$
$ ls -l out.nowritepermission
-r--------@ 1 clarke staff 22 27 Jan 15:13 out.nowritepermission
$ cat out.nowritepermission
"Immutable" contents.
$
$ swiftc permtest.swift
$ permtest
Are we allowed to write this file? -- no
$
$ ls -l out.nowritepermission
-r--------@ 1 clarke staff 24 27 Jan 15:14 out.nowritepermission
$ cat out.nowritepermission
This should not appear.
$

What are the permissions on the directory containing out.nowritepermission? The issue is likely the fact that you are writing atomically — the atomically flag causes the data to be written out to a temporary file, which the original file is then replaced by:

write(to:atomically:) | Apple Developer Documentation
If true , the data is written to a backup location, and then—assuming no errors occur—the backup location is renamed to the name specified by aURL ; otherwise, the data is written directly to aURL . atomically is ignored if aURL is not of a type the supports atomic writes.

The atomic part of that operation is that on UNIX systems, file renaming (on the same file system) is supposed to be atomic; in other words, you will either end up with the old file or the new file, but not some combination of data from one with the data of the other.

This means that for your use case, out.nowritepermission isn't opened for writing at all: instead, str is written out to a temporary file which then replaces out.nowritepermission. This can be done without write permission on the file itself, but with write permission on the containing directory.

You should be able to test this by also observing the file creation date if your file system supports it — the date should change between runs of permtest.swift. (You would also likely run into errors if it were impossible to write to the temporary file, since the call would try to open out.nowritepermission for writing and fail, but this condition is likely difficult to trigger directly.)

7 Likes

First, thanks! That was a thoughtful, careful reply that advanced my Swift education and reminded me of things I ought to have known about Unix.

You asked a couple of questions. Here are answers that I bet you knew already:

  • Yes, the enclosing directory has write permission.
  • Yes, the creation date is changed by the write. (ls -U does this on macOS. I tried on a Linux system too, but ls isn't so helpful there.)
  • Yes, write fails if the directory does not have write permission. (I'm writing a program that wants to write files. Writing to a non-writable directory was tested in a separate case and
    failed as expected.)

I still think this is counterintuitive. I expect there was an argument about this around 1970, and if I'd been there I'd have been on the losing side. But you work with what you've got, and I'm grateful you filled me in.

1 Like

Agreed! This aspect of UNIX permissions is definitely surprising and very counterintuitive, but indeed, it's what we've got for now. Happy to have helped.