I was going through the SAF filesystem implementation and noticed that safFilesystem.Create() currently calls:
return sfs.OpenFile(name, os.O_CREATE|os.O_TRUNC, 0)
Neither os.O_WRONLY nor os.O_RDWR is included. Since os.O_RDONLY is zero, this effectively requests read-only access.
safNode.OpenFile() uses those flags for two separate purposes:
- To choose the Android file descriptor mode.
- To set
canWrite on the returned safFile.
For Android's built-in external storage provider, the missing write flag selects mode "r". O_TRUNC then appends "t", producing "rt". Android's ParcelFileDescriptor.parseMode() accepts only "r", "w", "wt", "wa", "rw", and "rwt", so "rt" is invalid.
For other SAF providers, the underlying descriptor is opened as "rwt", but canWrite is still false because it is calculated only from O_WRONLY and O_RDWR.
Consequently, the built-in provider path fails while opening the descriptor, and the generic provider path returns a safFile whose Write(), WriteAt(), and Truncate() methods return os.ErrInvalid.
This also differs from Syncthing's BasicFilesystem.Create(), which opens the file with O_RDWR|O_CREATE|O_TRUNC.
The smallest fix appears to be:
return sfs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0)
A regression test could call Create() using mock document URIs that exercise both the built-in external storage and generic-provider paths, verify that "rwt" is requested, and write a few bytes through the returned file.
I have not reproduced the failure on-device, but the implementation itself appears deterministically broken. Syncthing does call Filesystem.Create() through osutil.copyFileContents(), although I have not determined how readily that path becomes user-visible for SAF folders.
This is present in BasicSync 3.7 / current master at commit 5b159a7.
I was going through the SAF filesystem implementation and noticed that
safFilesystem.Create()currently calls:Neither
os.O_WRONLYnoros.O_RDWRis included. Sinceos.O_RDONLYis zero, this effectively requests read-only access.safNode.OpenFile()uses those flags for two separate purposes:canWriteon the returnedsafFile.For Android's built-in external storage provider, the missing write flag selects mode
"r".O_TRUNCthen appends"t", producing"rt". Android'sParcelFileDescriptor.parseMode()accepts only"r","w","wt","wa","rw", and"rwt", so"rt"is invalid.For other SAF providers, the underlying descriptor is opened as
"rwt", butcanWriteis still false because it is calculated only fromO_WRONLYandO_RDWR.Consequently, the built-in provider path fails while opening the descriptor, and the generic provider path returns a
safFilewhoseWrite(),WriteAt(), andTruncate()methods returnos.ErrInvalid.This also differs from Syncthing's
BasicFilesystem.Create(), which opens the file withO_RDWR|O_CREATE|O_TRUNC.The smallest fix appears to be:
A regression test could call
Create()using mock document URIs that exercise both the built-in external storage and generic-provider paths, verify that"rwt"is requested, and write a few bytes through the returned file.I have not reproduced the failure on-device, but the implementation itself appears deterministically broken. Syncthing does call
Filesystem.Create()throughosutil.copyFileContents(), although I have not determined how readily that path becomes user-visible for SAF folders.This is present in BasicSync 3.7 / current master at commit
5b159a7.