1 /**
2     Validate and parse config files.
3 
4     Copyright: © 2019 Arne Ludwig <arne.ludwig@posteo.de>
5     License: Subject to the terms of the MIT license, as written in the
6              included LICENSE file.
7     Authors: Arne Ludwig <arne.ludwig@posteo.de>
8 */
9 module darg_plus.validators;
10 
11 
12 import darg_plus.exception : ValidationError;
13 static import std.exception;
14 
15 /**
16     General validation function.
17 
18     Params:
19         value  = Value to be validated
20         msg    = Error details in case of failure
21     Throws:
22         darg_plus.exception.ValidationError if `!isValid`.
23 */
24 alias validate = std.exception.enforce!ValidationError;
25 
26 /**
27     Validates that value is positive.
28 
29     Params:
30         value  = Value to be validated
31         msg    = Error details in case of failure
32     Throws:
33         darg_plus.exception.ValidationError
34             if value is less than or equal to zero.
35     See_also:
36         validate
37 */
38 void validatePositive(V)(V value, lazy string msg = "must be greater than zero")
39 {
40     validate(0 < value, msg);
41 }
42 
43 /**
44     Validates that the values described by rangeString are non-negative and
45     in ascending order. Validation is skipped if rangeString is `null`.
46 
47     Params:
48         rangeString  = Range to be validated
49         msg    = Error details in case of failure
50     Throws:
51         darg_plus.exception.ValidationError
52             unless rangeString is `null` or `0 <= x && x < y`.
53     See_also:
54         validate
55 */
56 void validateRangeNonNegativeAscending(DestType)(
57     in string rangeString,
58     lazy string msg = "0 <= <from> < <to> must hold",
59 )
60 {
61     if (rangeString !is null)
62     {
63         import darg_plus.string : parseRange;
64 
65         auto rangeBounds = parseRange!DestType(rangeString);
66         auto from = rangeBounds[0];
67         auto to = rangeBounds[1];
68 
69         validate(0 <= from && from < to, msg);
70     }
71 }
72 
73 
74 import std.range.primitives : isInputRange;
75 
76 /**
77     Validates that the files exist.
78 
79     Params:
80         file  = File name of the file to be tested.
81         files = File names of the files to be tested.
82         msg    = Error details in case of failure
83     Throws:
84         darg_plus.exception.ValidationError
85             unless rangeString is `null` or `0 <= x && x < y`.
86     See_also:
87         validate, std.file.exists
88 */
89 void validateFilesExist(R)(R files, lazy string msg = "cannot open file `%s`") if (isInputRange!R)
90 {
91     foreach (file; files)
92         validateFileExists(file, msg);
93 }
94 
95 /// ditto
96 void validateFileExists(S)(in S file, lazy string msg = "cannot open file `%s`")
97 {
98     import std.file : exists;
99     import std.format : format;
100 
101     validate(file.exists, format(msg, file));
102 }
103 
104 
105 import std.meta :
106     allSatisfy,
107     staticMap;
108 import std.traits : isSomeString;
109 
110 /**
111     Validates that the file has one of the allowed extensions.
112 
113     Params:
114         extensions  = Allowed extensions including a leading dot.
115         file        = File name of the file to be tested.
116         msg    = Error details in case of failure
117     Throws:
118         darg_plus.exception.ValidationError
119             if the extension of file is not in the list of allowed extensions.
120     See_also:
121         validate, std.algorithm.searching.endsWith
122 */
123 void validateFileExtension(extensions...)(
124     in string file,
125     lazy string msg = "invalid file extension: expected one of %-(%s, %) but got %s",
126 )
127         if (allSatisfy!(isSomeString, staticMap!(typeOf, extensions)))
128 {
129     import std.algorithm : endsWith;
130     import std.format : format;
131 
132     validate(file.endsWith(extensions), format(msg, [extensions], file));
133 }
134 
135 private alias typeOf(alias T) = typeof(T);
136 
137 /**
138     Validates that the file is writable.
139 
140     Params:
141         file        = File name of the file to be tested.
142         msg    = Error details in case of failure
143     Throws:
144         darg_plus.exception.ValidationError
145             if file cannot be opened for writing.
146     See_also:
147         validate, std.algorithm.searching.endsWith
148 */
149 void validateFileWritable(string file, lazy string msg = "cannot open file `%s` for writing: %s")
150 {
151     import std.exception : ErrnoException;
152     import std.file :
153         exists,
154         remove;
155     import std.format : format;
156     import std.stdio : File;
157 
158     auto deleteAfterwards = !file.exists;
159 
160     scope (exit)
161         if (deleteAfterwards)
162             remove(file);
163 
164     try
165     {
166         cast(void) File(file, "a");
167     }
168     catch (ErrnoException e)
169     {
170         validate(false, format(msg, file, e.msg));
171     }
172 }
173 
174 /**
175     Validates that `dir` is a directory and files can be created inside of it.
176 
177     Params:
178         dir    = Path of the directory to be tested.
179         msg    = Error details in case of failure
180     Throws:
181         darg_plus.exception.ValidationError
182             if `dir` is not a directory or files cannot be created within.
183     See_also:
184         validate, std.algorithm.searching.endsWith
185 */
186 void validateWritableDirectory(string dir, lazy string msg = "`%s` is not a writable directory: %s")
187 {
188     import core.sys.posix.stdlib : mkstemp;
189     import core.sys.posix.stdio :
190         fclose,
191         fdopen;
192     import std.exception :
193         errnoEnforce,
194         ErrnoException;
195     import std.file :
196         isDir,
197         remove;
198     import std.format : format;
199     import std.path : buildPath;
200     import std.string :
201         fromStringz,
202         toStringz;
203     import std.traits : ReturnType;
204 
205     validate(isDir(dir), format(msg, dir, "not a directory"));
206 
207     char* tempFileName;
208 
209     scope (exit)
210     {
211         if (tempFileName !is null)
212             remove(fromStringz(tempFileName));
213     }
214 
215     try
216     {
217         tempFileName = cast(char*) toStringz(buildPath(dir, ".iswritable-XXXXXX"));
218 
219         auto fd = mkstemp(tempFileName);
220 
221         errnoEnforce(fd != -1, "cannot create temporary file");
222 
223         fclose(fdopen(fd, "r+"));
224     }
225     catch (ErrnoException e)
226     {
227         validate(false, format(msg, dir, e.msg));
228     }
229 }