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