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 }