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.typecons; 25 import std.zip; 26 version(DubUseCurl) import std.net.curl; 27 28 29 Path getTempDir() 30 { 31 return Path(std.file.tempDir()); 32 } 33 34 private Path[] temporary_files; 35 36 Path getTempFile(string prefix, string extension = null) 37 { 38 import std.uuid : randomUUID; 39 40 auto path = getTempDir() ~ (prefix ~ "-" ~ randomUUID.toString() ~ extension); 41 temporary_files ~= path; 42 return path; 43 } 44 45 static ~this() 46 { 47 foreach (path; temporary_files) 48 { 49 std.file.remove(path.toNativeString()); 50 } 51 } 52 53 bool isEmptyDir(Path p) { 54 foreach(DirEntry e; dirEntries(p.toNativeString(), SpanMode.shallow)) 55 return false; 56 return true; 57 } 58 59 bool isWritableDir(Path p, bool create_if_missing = false) 60 { 61 import std.random; 62 auto fname = p ~ format("__dub_write_test_%08X", uniform(0, uint.max)); 63 if (create_if_missing && !exists(p.toNativeString())) mkdirRecurse(p.toNativeString()); 64 try openFile(fname, FileMode.CreateTrunc).close(); 65 catch (Exception) return false; 66 remove(fname.toNativeString()); 67 return true; 68 } 69 70 Json jsonFromFile(Path file, bool silent_fail = false) { 71 if( silent_fail && !existsFile(file) ) return Json.emptyObject; 72 auto f = openFile(file.toNativeString(), FileMode.Read); 73 scope(exit) f.close(); 74 auto text = stripUTF8Bom(cast(string)f.readAll()); 75 return parseJsonString(text, file.toNativeString()); 76 } 77 78 Json jsonFromZip(Path zip, string filename) { 79 auto f = openFile(zip, FileMode.Read); 80 ubyte[] b = new ubyte[cast(size_t)f.size]; 81 f.rawRead(b); 82 f.close(); 83 auto archive = new ZipArchive(b); 84 auto text = stripUTF8Bom(cast(string)archive.expand(archive.directory[filename])); 85 return parseJsonString(text, zip.toNativeString~"/"~filename); 86 } 87 88 void writeJsonFile(Path path, Json json) 89 { 90 auto f = openFile(path, FileMode.CreateTrunc); 91 scope(exit) f.close(); 92 f.writePrettyJsonString(json); 93 } 94 95 bool isPathFromZip(string p) { 96 enforce(p.length > 0); 97 return p[$-1] == '/'; 98 } 99 100 bool existsDirectory(Path path) { 101 if( !existsFile(path) ) return false; 102 auto fi = getFileInfo(path); 103 return fi.isDirectory; 104 } 105 106 void runCommands(in string[] commands, string[string] env = null) 107 { 108 foreach(cmd; commands){ 109 logDiagnostic("Running %s", cmd); 110 Pid pid; 111 if( env !is null ) pid = spawnShell(cmd, env); 112 else pid = spawnShell(cmd); 113 auto exitcode = pid.wait(); 114 enforce(exitcode == 0, "Command failed with exit code "~to!string(exitcode)); 115 } 116 } 117 118 /** 119 Downloads a file from the specified URL. 120 121 Any redirects will be followed until the actual file resource is reached or if the redirection 122 limit of 10 is reached. Note that only HTTP(S) is currently supported. 123 */ 124 void download(string url, string filename) 125 { 126 version(DubUseCurl) { 127 auto conn = HTTP(); 128 setupHTTPClient(conn); 129 logDebug("Storing %s...", url); 130 std.net.curl.download(url, filename, conn); 131 enforce(conn.statusLine.code < 400, 132 format("Failed to download %s: %s %s", 133 url, conn.statusLine.code, conn.statusLine.reason)); 134 } else version (Have_vibe_d) { 135 import vibe.inet.urltransfer; 136 vibe.inet.urltransfer.download(url, filename); 137 } else assert(false); 138 } 139 /// ditto 140 void download(URL url, Path filename) 141 { 142 download(url.toString(), filename.toNativeString()); 143 } 144 /// ditto 145 ubyte[] download(string url) 146 { 147 version(DubUseCurl) { 148 auto conn = HTTP(); 149 setupHTTPClient(conn); 150 logDebug("Getting %s...", url); 151 auto ret = cast(ubyte[])get(url, conn); 152 enforce(conn.statusLine.code < 400, 153 format("Failed to GET %s: %s %s", 154 url, conn.statusLine.code, conn.statusLine.reason)); 155 return ret; 156 } else version (Have_vibe_d) { 157 import vibe.inet.urltransfer; 158 import vibe.stream.operations; 159 ubyte[] ret; 160 download(url, (scope input) { ret = input.readAll(); }); 161 return ret; 162 } else assert(false); 163 } 164 /// ditto 165 ubyte[] download(URL url) 166 { 167 return download(url.toString()); 168 } 169 170 /// Returns the current DUB version in semantic version format 171 string getDUBVersion() 172 { 173 import dub.version_; 174 // convert version string to valid SemVer format 175 auto verstr = dubVersion; 176 if (verstr.startsWith("v")) verstr = verstr[1 .. $]; 177 auto parts = verstr.split("-"); 178 if (parts.length >= 3) { 179 // detect GIT commit suffix 180 if (parts[$-1].length == 8 && parts[$-1][1 .. $].isHexNumber() && parts[$-2].isNumber()) 181 verstr = parts[0 .. $-2].join("-") ~ "+" ~ parts[$-2 .. $].join("-"); 182 } 183 return verstr; 184 } 185 186 version(DubUseCurl) { 187 void setupHTTPClient(ref HTTP conn) 188 { 189 static if( is(typeof(&conn.verifyPeer)) ) 190 conn.verifyPeer = false; 191 192 auto proxy = environment.get("http_proxy", null); 193 if (proxy.length) conn.proxy = proxy; 194 195 conn.addRequestHeader("User-Agent", "dub/"~getDUBVersion()~" (std.net.curl; +https://github.com/rejectedsoftware/dub)"); 196 } 197 } 198 199 string stripUTF8Bom(string str) 200 { 201 if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] ) 202 return str[3 ..$]; 203 return str; 204 } 205 206 private bool isNumber(string str) { 207 foreach (ch; str) 208 switch (ch) { 209 case '0': .. case '9': break; 210 default: return false; 211 } 212 return true; 213 } 214 215 private bool isHexNumber(string str) { 216 foreach (ch; str) 217 switch (ch) { 218 case '0': .. case '9': break; 219 case 'a': .. case 'f': break; 220 case 'A': .. case 'F': break; 221 default: return false; 222 } 223 return true; 224 } 225 226 /** 227 Get the closest match of $(D input) in the $(D array), where $(D distance) 228 is the maximum levenshtein distance allowed between the compared strings. 229 Returns $(D null) if no closest match is found. 230 */ 231 string getClosestMatch(string[] array, string input, size_t distance) 232 { 233 import std.algorithm : countUntil, map, levenshteinDistance; 234 import std.uni : toUpper; 235 236 auto distMap = array.map!(elem => 237 levenshteinDistance!((a, b) => toUpper(a) == toUpper(b))(elem, input)); 238 auto idx = distMap.countUntil!(a => a <= distance); 239 return (idx == -1) ? null : array[idx]; 240 } 241 242 /** 243 Searches for close matches to input in range. R must be a range of strings 244 Note: Sorts the strings range. Use std.range.indexed to avoid this... 245 */ 246 auto fuzzySearch(R)(R strings, string input){ 247 import std.algorithm : levenshteinDistance, schwartzSort, partition3; 248 import std.traits : isSomeString; 249 import std.range : ElementType; 250 251 static assert(isSomeString!(ElementType!R), "Cannot call fuzzy search on non string rang"); 252 immutable threshold = input.length / 4; 253 return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1] 254 .schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper)); 255 }