1 /**
2  * Open $(B std.stdio.File) using a set of symbolic constants as a file access mode.
3  * Authors:
4  *  $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
5  * Copyright:
6  *  Roman Chistokhodov, 2020
7  * License:
8  *  $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
9  */
10 
11 module openfile;
12 
13 public import std.stdio : File;
14 
15 /// Flags for file open mode.
16 enum OpenMode
17 {
18     /// Open in read mode.
19     read = 1 << 0,
20     /**
21         * Open in write mode, dont' truncate.
22         * Create a file if `existingOnly` flag is not provided.
23         */
24     update = 1 << 1,
25     /**
26         * Open in write mode, truncate if exists.
27         * Create a file if `existingOnly` flag is not provided.
28         */
29     truncate = 1 << 2,
30     /**
31         * Open in write mode. Append to the end on writing.
32         * Create a file if `existingOnly` flag is not provided.
33         *
34         * Note that it has a special meaning on Posix:
35         * the file opened in append mode will have write operations
36         * happening at the end of the file regardless of manual seek position changing.
37         */
38     append = 1 << 3,
39     /**
40         * Open in write mode. Create file only if it does not exist, error otherwise.
41         * The check for existence and the file creation is an atomical operation.
42         * Use this flag when need to ensure that the file with such name did not exist.
43         */
44     createNew = 1 << 4,
45     /**
46         * Don't create file if it does not exist when opening in write mode.
47         * This flag is not necessary when opening a file in read-only mode.
48         * This flag can't be used together with `OpenMode.createNew` flag.
49         */
50     existingOnly = 1 << 5,
51 }
52 
53 private bool hasAnyWriteFlag(OpenMode openMode) @safe nothrow pure @nogc
54 {
55     with(OpenMode) return (openMode & (update | truncate | append | createNew)) != 0;
56 }
57 
58 private string modezFromOpenMode(OpenMode openMode) @safe
59 {
60     const bool anyWrite = hasAnyWriteFlag(openMode);
61     if (openMode & OpenMode.read)
62     {
63         if (anyWrite)
64         {
65             if (openMode & OpenMode.append)
66                 return "a+";
67             else if (openMode & OpenMode.existingOnly)
68                 return "r+";
69             else
70                 return "w+";
71         }
72         else return "r";
73     }
74     else
75     {
76         assert(anyWrite);
77         if (openMode & OpenMode.append)
78             return "a";
79         else
80             return "w";
81     }
82 }
83 
84 version(Posix)
85 {
86 private:
87     import core.sys.posix.fcntl : O_RDWR, O_RDONLY, O_WRONLY, O_TRUNC, O_APPEND, O_EXCL, O_CREAT, mode_t;
88 
89     mode_t unixModeFromOpenMode(OpenMode openMode) @safe @nogc nothrow pure
90     {
91         mode_t mode;
92         const bool anyWrite = hasAnyWriteFlag(openMode);
93         const bool existing = (openMode & OpenMode.existingOnly) != 0;
94         const bool hasRead = (openMode & OpenMode.read) != 0;
95         if (hasRead && anyWrite)
96             mode |= O_RDWR;
97         else if (hasRead)
98             mode |= O_RDONLY;
99         else if (anyWrite)
100             mode |= O_WRONLY;
101 
102         if (anyWrite && !existing)
103             mode |= O_CREAT;
104 
105         if (openMode & OpenMode.truncate)
106             mode |= O_TRUNC;
107         if (openMode & OpenMode.append)
108             mode |= O_APPEND;
109         if (openMode & OpenMode.createNew)
110             mode |= O_EXCL;
111         return mode;
112     }
113 
114     unittest
115     {
116         assert(unixModeFromOpenMode(OpenMode.read | OpenMode.truncate) == (O_RDWR | O_TRUNC | O_CREAT));
117     }
118 
119     int openFd(string name, OpenMode openMode) @trusted
120     {
121         import std.exception : errnoEnforce;
122         import std.internal.cstring : tempCString;
123         import std.conv : octal;
124         static import core.sys.posix.fcntl;
125 
126         mode_t mode = unixModeFromOpenMode(openMode);
127 
128         auto namez = name.tempCString();
129         int fd;
130         if (mode & O_CREAT)
131             fd = core.sys.posix.fcntl.open(namez, mode, octal!666);
132         else
133             fd = core.sys.posix.fcntl.open(namez, mode);
134         errnoEnforce(fd >= 0);
135         return fd;
136     }
137 }
138 
139 version(Windows)
140 {
141 private:
142     import core.sys.windows.core : HANDLE;
143 
144     HANDLE openHandle(string name, OpenMode openMode) @trusted
145     {
146         import std.utf : toUTF16z;
147         import std.windows.syserror : wenforce;
148         import core.sys.windows.core : FILE_ATTRIBUTE_NORMAL, FILE_FLAG_SEQUENTIAL_SCAN,
149                                         GENERIC_READ, GENERIC_WRITE, FILE_SHARE_READ, FILE_SHARE_WRITE,
150                                         TRUNCATE_EXISTING, OPEN_EXISTING, CREATE_ALWAYS,
151                                         CREATE_NEW, OPEN_ALWAYS, INVALID_HANDLE_VALUE,
152                                         FILE_END, INVALID_SET_FILE_POINTER, DWORD,
153                                         CreateFileW, SetFilePointer;
154 
155         auto namez = name.toUTF16z;
156         const bool anyWrite = hasAnyWriteFlag(openMode);
157         const bool existing = (openMode & OpenMode.existingOnly) != 0;
158         const bool hasRead = (openMode & OpenMode.read) != 0;
159 
160         DWORD desiredAccess = 0;
161         DWORD shareMode = 0;
162         DWORD creationDisposition = 0;
163         DWORD flags = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN;
164 
165         if (hasRead)
166         {
167             desiredAccess |= GENERIC_READ;
168             shareMode |= FILE_SHARE_READ;
169         }
170         if (anyWrite)
171         {
172             desiredAccess |= GENERIC_WRITE;
173             shareMode |= FILE_SHARE_WRITE;
174         }
175 
176         if ((hasRead && !anyWrite) || existing)
177         {
178             if (openMode & OpenMode.truncate)
179                 creationDisposition = TRUNCATE_EXISTING;
180             else
181                 creationDisposition = OPEN_EXISTING;
182         }
183         else if (openMode & OpenMode.createNew)
184         {
185             creationDisposition = CREATE_NEW;
186         }
187         else if (openMode & OpenMode.truncate)
188         {
189             creationDisposition = CREATE_ALWAYS;
190         }
191         else if (anyWrite)
192         {
193             creationDisposition = OPEN_ALWAYS;
194         }
195         HANDLE h = CreateFileW(namez, desiredAccess, shareMode, null, creationDisposition, flags, HANDLE.init);
196         wenforce(h != INVALID_HANDLE_VALUE, name);
197         if (openMode & OpenMode.append)
198             wenforce(SetFilePointer(h, 0, null, FILE_END) != INVALID_SET_FILE_POINTER, name);
199         return h;
200     }
201 }
202 
203 /**
204  * Open file using name and symbolic access mode.
205  * See_Also: $(D openfile.sopen)
206  */
207 File openFile(string name, OpenMode mode) @trusted
208 {
209     import std.exception : enforce;
210     enforce((mode & OpenMode.read) != 0 || hasAnyWriteFlag(mode),
211             "read flag or some of write flags must be provided in open mode");
212     enforce(!((mode & OpenMode.createNew) != 0 && (mode & OpenMode.existingOnly) != 0),
213             "createNew and existingOnly can't be used together in open mode");
214 
215     version(Posix)
216     {
217         import core.sys.posix.unistd : close;
218         int fd = openFd(name, mode);
219         scope(failure) close(fd);
220         File file;
221         file.fdopen(fd, modezFromOpenMode(mode));
222         return file;
223     }
224     else version(Windows)
225     {
226         import core.sys.windows.core : CloseHandle;
227         HANDLE handle = openHandle(name, mode);
228         scope(failure) CloseHandle(handle);
229         File file;
230         file.windowsHandleOpen(handle, modezFromOpenMode(mode));
231         return file;
232     }
233 }
234 
235 /// Open file using name and symbolic access mode. Convenient function for UFCS. Calls $(B std.stdio.detach) before assigning a new file handle.
236 void sopen(ref scope File file, string name, OpenMode mode) @safe
237 {
238     file.detach();
239     file = openFile(name, mode);
240 }
241 
242 ///
243 unittest
244 {
245     static import std.file;
246     import std.path : buildPath;
247     import std.exception : assertThrown;
248     import std.process : thisProcessID;
249     import std.conv : to;
250 
251     auto deleteme = buildPath(std.file.tempDir(), "deleteme.openfile.unittest.pid" ~ to!string(thisProcessID));
252     scope(exit) std.file.remove(deleteme);
253 
254     // bad set of flags
255     assertThrown(openFile(deleteme, OpenMode.createNew | OpenMode.existingOnly));
256     assertThrown(openFile(deleteme, OpenMode.existingOnly));
257 
258     // opening non-existent file
259     assertThrown(openFile(deleteme, OpenMode.read));
260     assertThrown(openFile(deleteme, OpenMode.update | OpenMode.existingOnly));
261 
262     File f = openFile(deleteme, OpenMode.read | OpenMode.truncate | OpenMode.createNew);
263     f.write("Hello");
264     f.rewind();
265     assert(f.readln() == "Hello");
266 
267     assertThrown(openFile(deleteme, OpenMode.createNew));
268 
269     f.sopen(deleteme, OpenMode.append | OpenMode.existingOnly);
270     f.write(" world");
271 
272     f.sopen(deleteme, OpenMode.update | OpenMode.existingOnly);
273     f.seek(6);
274     f.write("sco");
275 
276     f.sopen(deleteme, OpenMode.read);
277     assert(f.readln() == "Hello scold");
278 
279     f.sopen(deleteme, OpenMode.read | OpenMode.update | OpenMode.existingOnly);
280     f.write("Yo");
281     f.rewind();
282     assert(f.readln() == "Yollo scold");
283 
284     f.sopen(deleteme, OpenMode.read | OpenMode.append | OpenMode.existingOnly);
285     f.write("ing");
286     f.rewind();
287     assert(f.readln() == "Yollo scolding");
288 
289     auto deleteme2 = buildPath(std.file.tempDir(), "deleteme2.openfile.unittest.pid" ~ to!string(thisProcessID));
290     scope(exit) std.file.remove(deleteme2);
291 
292     assertThrown(f.sopen(deleteme2, OpenMode.truncate | OpenMode.existingOnly));
293 
294     f.sopen(deleteme2, OpenMode.read | OpenMode.update | OpenMode.createNew);
295     f.write("baz");
296     f.rewind();
297     assert(f.readln() == "baz");
298     f.seek(3);
299     f.write("bar");
300     f.rewind();
301     assert(f.readln() == "bazbar");
302 
303     f.sopen(deleteme2, OpenMode.read | OpenMode.truncate | OpenMode.existingOnly);
304     f.write("some");
305     f.rewind();
306     assert(f.readln() == "some");
307 
308     f.close();
309 }