1 module rest.apiv1;
2 
3 import vibe.d;
4 import rest.iapiv1;
5 import exec.iexecprovider;
6 import contentprovider;
7 
8 class ApiV1: IApiV1
9 {
10 	private IExecProvider execProvider_;
11 	private ContentProvider contentProvider_;
12 	string githubToken; /// Token for authenticating GitHub requests
13 
14 	this(IExecProvider execProvider, ContentProvider contentProvider, string githubToken)
15 	{
16 		this.execProvider_ = execProvider;
17 		this.contentProvider_ = contentProvider;
18 		this.githubToken = "token " ~ githubToken;
19 	}
20 
21 	/++
22 		Parses the message contained in $(D output)
23 		and fills the appropriate errors and warnings
24 		arrays.
25 		Note: parsing s just done when $(D output.success)
26 		is false.
27 	+/
28 	private static void parseErrorsAndWarnings(ref RunOutput output)
29 	{
30 		import std.regex: ctRegex, matchFirst, replaceAll;
31 		import std.conv: to;
32 		import std..string: lineSplitter;
33 
34 		static coloring = ctRegex!
35 			`\x1B\[[0-9;]*[mGK]`;
36 		static ctr = ctRegex!
37 			`^[^(]+\(([0-9]+)(,[0-9]+)?\): ([a-zA-Z]+): (.*)$`;
38 
39 		foreach(line; output.output.replaceAll(coloring, "").lineSplitter) {
40 			auto m = line.matchFirst(ctr);
41 			if (m.empty)
42 				continue;
43 			auto lineNumber = to!int(m[1]);
44 			string type = m[3];
45 			string message = m[4];
46 
47 			switch (type) {
48 				case "Warning":
49 				case "Deprecation":
50 					output.warnings ~= RunOutput.Message(lineNumber,
51 						message);
52 					break;
53 				default:
54 					output.errors ~= RunOutput.Message(lineNumber,
55 						message);
56 			}
57 		}
58 	}
59 
60 	// --- Argumentation for the chosen limit ---
61 	// The average line length of Phobos std/ is about 30 characters,
62 	// while on average files are 1800 lines long. The average file
63 	// size is 62391 bytes.
64 	// We pick a rather generous limit of 64k (64 * 1024) which ought
65 	// to be enough for everybody.
66 	enum uint maxSourceCodeLength = 64 * 1024;
67 
68 	RunOutput run(ApiV1.RunInput input)
69 	{
70 		if (input.source.length > maxSourceCodeLength) {
71 			return RunOutput("ERROR: source code size is above limit of 64k bytes.", false);
72 		}
73 
74 		// sanitize input and remove weird unicode characters
75 		input.source = input.source.removeUnicodeSpaces;
76 
77 		// Be explicit here, in case the exec API and the REST API change
78 		IExecProvider.RunInput runInput = {
79 			source: input.source,
80 			compiler : input.compiler,
81 			args: input.args,
82 			stdin: input.stdin,
83 			color: input.color,
84 		};
85 		auto result = execProvider_.compileAndExecute(runInput);
86 		auto output = RunOutput(result.output, result.success);
87 		parseErrorsAndWarnings(output);
88 		return output;
89 	}
90 
91 	FormatOutput format(string source)
92 	{
93 		// https://github.com/dlang-community/dfmt/blob/master/src/dfmt/config.d
94 		import dfmt.config : Config;
95 		import dfmt.formatter : format;
96 		import std.array : appender;
97 		import std.range.primitives;
98 
99 		FormatOutput output;
100 		ubyte[] buffer;
101 		buffer = cast(ubyte[]) source;
102 
103 		Config formatterConfig;
104 		formatterConfig.initializeWithDefaults();
105 		auto app = appender!string;
106 		format(source, buffer, app, &formatterConfig);
107 		output.source = app.data;
108 		if (output.source.back == '\n')
109 			output.source.popBack;
110 		return output;
111 	}
112 
113 	ShortenOutput shorten(string source, string compiler, string args)
114 	{
115 		import std.format : format;
116 		import std.uri : encodeComponent;
117 
118 		ShortenOutput output;
119 		auto url = "https://run.dlang.io?compiler=%s&source=%s".format(compiler, source.encodeComponent);
120 		if (args.length > 0)
121 			url ~= "&args=" ~ args.encodeComponent;
122 
123 		auto isURL= "https://is.gd/create.php?format=simple&url=%s".format(url.encodeComponent);
124 		output.url = requestHTTP(isURL, (scope req) {
125 			req.method = HTTPMethod.POST;
126 		}).bodyReader.readAllUTF8.replace("https://is.gd/", "https://run.dlang.io/is/");
127 		output.success = true;
128 		return output;
129 	}
130 
131 	GistOutput gist(string source, string compiler, string args)
132 	{
133 		import std.format : format;
134 		import std.uri : encodeComponent;
135 
136 		GistOutput output;
137 
138 	    auto res = requestHTTP("https://api.github.com/gists", (scope req) {
139 			req.headers["Authorization"] = githubToken;
140 			Json data = Json.emptyObject;
141 			data["description"] = "Code shared from run.dlang.io.%s".format(args ?
142 				" Run with '%s'".format(args) : "");
143 			data["public"] = true;
144 			Json files = Json.emptyObject;
145 			files["main.d"] = Json([
146 				"content": Json(source),
147 			]);
148 			data["files"] = files;
149 			req.method = HTTPMethod.POST;
150             req.writeJsonBody(data);
151 		});
152 		enforceHTTP(res.statusCode / 100 == 2, HTTPStatus.internalServerError, "GitHub API not available.");
153 		auto json = res.readJson;
154 		auto gistId = json["id"].get!string;
155         logInfo("Gist created: %s", gistId);
156 		output.id = gistId;
157 		output.htmlUrl = json["html_url"].get!string;
158 
159 		output.url = "/gist/" ~ gistId;
160 		if (compiler != "dmd") {
161 			output.url ~= "?compiler=%s".format(compiler);
162 			if (args.length > 0)
163 				output.url ~= "&args=" ~ args.encodeComponent;
164 		} else if (args.length > 0) {
165 			output.url ~= "?args=" ~ args.encodeComponent;
166 		}
167 
168 		return output;
169 	}
170 
171 unittest
172 {
173 	string source = `void main() {}`;
174 	ApiV1 api = new ApiV1(null, null, null);
175 	auto res = api.format(source);
176 	assert(res == FormatOutput("void main()\n{\n}", false));
177 }
178 
179 	SourceOutput getSource(string _language, string _chapter, string _section)
180 	{
181 		auto tourData = contentProvider_.getContent(_language, _chapter, _section);
182 		if (tourData.content == null) {
183 			throw new HTTPStatusException(404,
184 				"Couldn't find tour data for chapter '%s', section %d".format(_language, _chapter, _section));
185 		}
186 
187 		return SourceOutput(tourData.content.sourceCode);
188 	}
189 }
190 
191 unittest {
192 	string run1 = `onlineapp.d(6): Error: found '}' when expecting ';' following statement
193 onlineapp.d(6): Error: found 'EOF' when expecting '}' following compound statement
194 Failed: ["dmd", "-v", "-o-", "onlineapp.d", "-I."]`;
195 
196 	auto test2 = ApiV1.RunOutput(run1, false);
197 	ApiV1.parseErrorsAndWarnings(test2);
198 	assert(test2.errors.length == 2);
199 	assert(test2.errors[0].line == 6);
200 	assert(test2.errors[0].message[0 .. 9] == "found '}'");
201 	assert(test2.errors[1].line == 6);
202 	assert(test2.errors[1].message[0 .. 11] == "found 'EOF'");
203 
204 	string run2 = `dlang-tour ~master: building configuration "executable"...
205 ../.dub/packages/dyaml-0.5.2/source/dyaml/dumper.d(15,8): Deprecation: module std.stream is deprecated - It will be removed from Phobos in October 2016. If you still need it, go to https://github.com/DigitalMars/undeaD
206 ../.dub/packages/dyaml-0.5.2/source/dyaml/emitter.d(21,8): Deprecation: module std.stream is deprecated - It will be removed from Phobos in October 2016. If you still need it, go to https://github.com/DigitalMars/undeaD
207 ../.dub/packages/dyaml-0.5.2/source/dyaml/representer.d(679,8): Deprecation: module std.stream is deprecated - It will be removed from Phobos in October 2016. If you still need it, go to https://github.com/DigitalMars/undeaD
208 ../.dub/packages/dyaml-0.5.2/source/dyaml/loader.d(171,16): Deprecation: module std.stream is deprecated - It will be removed from Phobos in October 2016. If you still need it, go to https://github.com/DigitalMars/undeaD`;
209 	auto test3 = ApiV1.RunOutput(run2, false);
210 	ApiV1.parseErrorsAndWarnings(test3);
211 	assert(test3.warnings.length == 4);
212 	import std.algorithm: all;
213 	assert(test3.warnings.all!(x => x.message[0 .. 31] == "module std.stream is deprecated"));
214 	assert(test3.warnings[0].line == 15);
215 	assert(test3.warnings[1].line == 21);
216 	assert(test3.warnings[2].line == 679);
217 	assert(test3.warnings[3].line == 171);
218 
219 	auto run3 =
220 	"\u001B[1md.d(1): \u001B[1;31mError: \u001B[mbasic type expected, not {\n" ~
221 	"\u001B[1md.d(1): \u001B[1;31mError: \u001B[mfound '{' when expecting ')'\n" ~
222 	"\u001B[1md.d(1): \u001B[1;31mError: \u001B[msemicolon expected following function declaration\n" ~
223 	"\u001B[1md.d(1): \u001B[1;31mError: \u001B[munrecognized declaration\n";
224 
225 	auto test4 = ApiV1.RunOutput(run3, false);
226 	ApiV1.parseErrorsAndWarnings(test4);
227 
228 	assert(test4.errors.length == 4);
229 	assert(test4.errors[0] ==
230 	       ApiV1.RunOutput.Message(1, "basic type expected, not {"));
231 	assert(test4.errors[1] ==
232 	       ApiV1.RunOutput.Message(1, "found '{' when expecting ')'"));
233 	assert(test4.errors[2] ==
234 	       ApiV1.RunOutput.Message(1, "semicolon expected following function declaration"));
235 	assert(test4.errors[3] ==
236 	       ApiV1.RunOutput.Message(1, "unrecognized declaration"));
237 }
238 
239 // remove weird unicode spaces
240 private string removeUnicodeSpaces(S)(S input) {
241 	import std.algorithm.iteration : map;
242 	import std.array : array;
243 	import std.uni : isSpace;
244 	// selective import will trigger false-positive deprecation warning
245 	import std.utf;
246 	return (cast(dstring) input.map!(c => c.isSpace ? ' ' : c).array).toUTF8;
247 }
248 
249 // weird 0xa0 unicode space tokens on android
250 unittest {
251 	import std..string : representation;
252 	ubyte[] helloWorldNormal = [10, 32, 32, 32, 32, 32, 32, 119, 114, 105];
253 	ubyte[] helloWorldAndroid = [10, 32, 194, 160, 32, 194, 160, 32, 32, 119, 114, 105];
254 	string res = cast(string) helloWorldAndroid;
255 	assert(res.removeUnicodeSpaces.representation == helloWorldNormal);
256 }