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 }