1 /** 2 ... 3 4 Copyright: © 2012 Matthias Dondorff 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Matthias Dondorff 7 */ 8 module dub.internal.utils; 9 10 import dub.internal.vibecompat.core.file; 11 import dub.internal.vibecompat.core.log; 12 import dub.internal.vibecompat.data.json; 13 import dub.internal.vibecompat.inet.url; 14 import dub.version_; 15 16 // todo: cleanup imports. 17 import std.algorithm : startsWith; 18 import std.array; 19 import std.conv; 20 import std.exception; 21 import std.file; 22 import std.process; 23 import std..string; 24 import std.traits : isIntegral; 25 import std.typecons; 26 import std.zip; 27 version(DubUseCurl) import std.net.curl; 28 29 30 Path getTempDir() 31 { 32 return Path(std.file.tempDir()); 33 } 34 35 private Path[] temporary_files; 36 37 Path getTempFile(string prefix, string extension = null) 38 { 39 import std.uuid : randomUUID; 40 41 auto path = getTempDir() ~ (prefix ~ "-" ~ randomUUID.toString() ~ extension); 42 temporary_files ~= path; 43 return path; 44 } 45 46 static ~this() 47 { 48 foreach (path; temporary_files) 49 { 50 std.file.remove(path.toNativeString()); 51 } 52 } 53 54 bool isEmptyDir(Path p) { 55 foreach(DirEntry e; dirEntries(p.toNativeString(), SpanMode.shallow)) 56 return false; 57 return true; 58 } 59 60 bool isWritableDir(Path p, bool create_if_missing = false) 61 { 62 import std.random; 63 auto fname = p ~ format("__dub_write_test_%08X", uniform(0, uint.max)); 64 if (create_if_missing && !exists(p.toNativeString())) mkdirRecurse(p.toNativeString()); 65 try openFile(fname, FileMode.CreateTrunc).close(); 66 catch (Exception) return false; 67 remove(fname.toNativeString()); 68 return true; 69 } 70 71 Json jsonFromFile(Path file, bool silent_fail = false) { 72 if( silent_fail && !existsFile(file) ) return Json.emptyObject; 73 auto f = openFile(file.toNativeString(), FileMode.Read); 74 scope(exit) f.close(); 75 auto text = stripUTF8Bom(cast(string)f.readAll()); 76 return parseJsonString(text, file.toNativeString()); 77 } 78 79 Json jsonFromZip(Path zip, string filename) { 80 auto f = openFile(zip, FileMode.Read); 81 ubyte[] b = new ubyte[cast(size_t)f.size]; 82 f.rawRead(b); 83 f.close(); 84 auto archive = new ZipArchive(b); 85 auto text = stripUTF8Bom(cast(string)archive.expand(archive.directory[filename])); 86 return parseJsonString(text, zip.toNativeString~"/"~filename); 87 } 88 89 void writeJsonFile(Path path, Json json) 90 { 91 auto f = openFile(path, FileMode.CreateTrunc); 92 scope(exit) f.close(); 93 f.writePrettyJsonString(json); 94 } 95 96 bool isPathFromZip(string p) { 97 enforce(p.length > 0); 98 return p[$-1] == '/'; 99 } 100 101 bool existsDirectory(Path path) { 102 if( !existsFile(path) ) return false; 103 auto fi = getFileInfo(path); 104 return fi.isDirectory; 105 } 106 107 void runCommands(in string[] commands, string[string] env = null) 108 { 109 import std.stdio : stdin, stdout, stderr, File; 110 111 version(Windows) enum nullFile = "NUL"; 112 else version(Posix) enum nullFile = "/dev/null"; 113 else static assert(0); 114 115 auto childStdout = stdout; 116 auto childStderr = stderr; 117 auto config = Config.retainStdout | Config.retainStderr; 118 119 // Disable child's stdout/stderr depending on LogLevel 120 auto logLevel = getLogLevel(); 121 if(logLevel >= LogLevel.warn) 122 childStdout = File(nullFile, "w"); 123 if(logLevel >= LogLevel.none) 124 childStderr = File(nullFile, "w"); 125 126 foreach(cmd; commands){ 127 logDiagnostic("Running %s", cmd); 128 Pid pid; 129 pid = spawnShell(cmd, stdin, childStdout, childStderr, env, config); 130 auto exitcode = pid.wait(); 131 enforce(exitcode == 0, "Command failed with exit code "~to!string(exitcode)); 132 } 133 } 134 135 /** 136 Downloads a file from the specified URL. 137 138 Any redirects will be followed until the actual file resource is reached or if the redirection 139 limit of 10 is reached. Note that only HTTP(S) is currently supported. 140 */ 141 void download(string url, string filename) 142 { 143 version(DubUseCurl) { 144 auto conn = HTTP(); 145 setupHTTPClient(conn); 146 logDebug("Storing %s...", url); 147 std.net.curl.download(url, filename, conn); 148 enforce(conn.statusLine.code < 400, 149 format("Failed to download %s: %s %s", 150 url, conn.statusLine.code, conn.statusLine.reason)); 151 } else version (Have_vibe_d) { 152 import vibe.inet.urltransfer; 153 vibe.inet.urltransfer.download(url, filename); 154 } else assert(false); 155 } 156 /// ditto 157 void download(URL url, Path filename) 158 { 159 download(url.toString(), filename.toNativeString()); 160 } 161 /// ditto 162 ubyte[] download(string url) 163 { 164 version(DubUseCurl) { 165 auto conn = HTTP(); 166 setupHTTPClient(conn); 167 logDebug("Getting %s...", url); 168 auto ret = cast(ubyte[])get(url, conn); 169 enforce(conn.statusLine.code < 400, 170 format("Failed to GET %s: %s %s", 171 url, conn.statusLine.code, conn.statusLine.reason)); 172 return ret; 173 } else version (Have_vibe_d) { 174 import vibe.inet.urltransfer; 175 import vibe.stream.operations; 176 ubyte[] ret; 177 download(url, (scope input) { ret = input.readAll(); }); 178 return ret; 179 } else assert(false); 180 } 181 /// ditto 182 ubyte[] download(URL url) 183 { 184 return download(url.toString()); 185 } 186 187 /// Returns the current DUB version in semantic version format 188 string getDUBVersion() 189 { 190 import dub.version_; 191 // convert version string to valid SemVer format 192 auto verstr = dubVersion; 193 if (verstr.startsWith("v")) verstr = verstr[1 .. $]; 194 auto parts = verstr.split("-"); 195 if (parts.length >= 3) { 196 // detect GIT commit suffix 197 if (parts[$-1].length == 8 && parts[$-1][1 .. $].isHexNumber() && parts[$-2].isNumber()) 198 verstr = parts[0 .. $-2].join("-") ~ "+" ~ parts[$-2 .. $].join("-"); 199 } 200 return verstr; 201 } 202 203 version(DubUseCurl) { 204 void setupHTTPClient(ref HTTP conn) 205 { 206 static if( is(typeof(&conn.verifyPeer)) ) 207 conn.verifyPeer = false; 208 209 auto proxy = environment.get("http_proxy", null); 210 if (proxy.length) conn.proxy = proxy; 211 212 conn.addRequestHeader("User-Agent", "dub/"~getDUBVersion()~" (std.net.curl; +https://github.com/rejectedsoftware/dub)"); 213 } 214 } 215 216 string stripUTF8Bom(string str) 217 { 218 if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] ) 219 return str[3 ..$]; 220 return str; 221 } 222 223 private bool isNumber(string str) { 224 foreach (ch; str) 225 switch (ch) { 226 case '0': .. case '9': break; 227 default: return false; 228 } 229 return true; 230 } 231 232 private bool isHexNumber(string str) { 233 foreach (ch; str) 234 switch (ch) { 235 case '0': .. case '9': break; 236 case 'a': .. case 'f': break; 237 case 'A': .. case 'F': break; 238 default: return false; 239 } 240 return true; 241 } 242 243 /** 244 Get the closest match of $(D input) in the $(D array), where $(D distance) 245 is the maximum levenshtein distance allowed between the compared strings. 246 Returns $(D null) if no closest match is found. 247 */ 248 string getClosestMatch(string[] array, string input, size_t distance) 249 { 250 import std.algorithm : countUntil, map, levenshteinDistance; 251 import std.uni : toUpper; 252 253 auto distMap = array.map!(elem => 254 levenshteinDistance!((a, b) => toUpper(a) == toUpper(b))(elem, input)); 255 auto idx = distMap.countUntil!(a => a <= distance); 256 return (idx == -1) ? null : array[idx]; 257 } 258 259 /** 260 Searches for close matches to input in range. R must be a range of strings 261 Note: Sorts the strings range. Use std.range.indexed to avoid this... 262 */ 263 auto fuzzySearch(R)(R strings, string input){ 264 import std.algorithm : levenshteinDistance, schwartzSort, partition3; 265 import std.traits : isSomeString; 266 import std.range : ElementType; 267 268 static assert(isSomeString!(ElementType!R), "Cannot call fuzzy search on non string rang"); 269 immutable threshold = input.length / 4; 270 return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1] 271 .schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper)); 272 } 273 274 /** 275 If T is a bitfield-style enum, this function returns a string range 276 listing the names of all members included in the given value. 277 278 Example: 279 --------- 280 enum Bits { 281 none = 0, 282 a = 1<<0, 283 b = 1<<1, 284 c = 1<<2, 285 a_c = a | c, 286 } 287 288 assert( bitFieldNames(Bits.none).equals(["none"]) ); 289 assert( bitFieldNames(Bits.a).equals(["a"]) ); 290 assert( bitFieldNames(Bits.a_c).equals(["a", "c", "a_c"]) ); 291 --------- 292 */ 293 auto bitFieldNames(T)(T value) if(is(T==enum) && isIntegral!T) 294 { 295 import std.algorithm : filter, map; 296 import std.conv : to; 297 import std.traits : EnumMembers; 298 299 return [ EnumMembers!(T) ] 300 .filter!(member => member==0? value==0 : (value & member) == member) 301 .map!(member => to!string(member)); 302 }