1 | import java.io.*;
|
2 | import java.net.URL;
|
3 | import java.net.URLConnection;
|
4 | import java.security.MessageDigest;
|
5 | import java.security.NoSuchAlgorithmException;
|
6 | import java.util.ArrayList;
|
7 | import java.util.List;
|
8 | import java.util.regex.Matcher;
|
9 | import java.util.regex.Pattern;
|
10 |
|
11 | /**
|
12 | Baked single-source-file version of javax3 (multiple class files are unavoidable...)
|
13 |
|
14 | javax3 accepts a snippet ID as well as a directory name as argument.
|
15 | */
|
16 |
|
17 | public class javax3 {
|
18 | static boolean verbose = false;
|
19 |
|
20 | private static String isCompilable = "http://tinybrain.de/"
|
21 | + /*"564"*/"1000251"; // silenced version (but I think we're silencing now anyway)
|
22 | private static String isCompilableHash = "8fe3851d7a686b60546a2429a2bde0d9";
|
23 |
|
24 | private static String mainAdder = "http://tinybrain.de/1000252";
|
25 | private static String mainAdderHash = "ce1e89785facd87ea64a6d9ad16a7aec";
|
26 |
|
27 | public static void main(String[] args) throws IOException {
|
28 | File ioBaseDir = new File(".");
|
29 | String src = ".";
|
30 | for (int i = 0; i < args.length; i++) {
|
31 | String arg = args[i];
|
32 | if (arg.equals("-v"))
|
33 | verbose = true;
|
34 | else
|
35 | src = arg;
|
36 | }
|
37 |
|
38 | javax3(src, ioBaseDir);
|
39 | }
|
40 |
|
41 | public static void javax3(String src, File ioDir) throws IOException {
|
42 | File srcDir;
|
43 | if (isSnippetID(src))
|
44 | srcDir = loadSnippetAsMainJava(src);
|
45 | else
|
46 | srcDir = new File(src);
|
47 | Input X = programToInput(srcDir);
|
48 | boolean compilable = isCompilable(X);
|
49 | if (verbose) System.out.println("Compilable: " + compilable);
|
50 | if (!compilable) {
|
51 | X = expandProgram(X);
|
52 | File file = new File(X.dir, "main.java");
|
53 | String program = loadTextFile(file.getPath(), null);
|
54 | if (verbose) System.out.println("New program:\n" + program);
|
55 | if (program == null)
|
56 | throw new RuntimeException("Expansion failed (tried: " + file.getPath() + ")");
|
57 | }
|
58 | javax2(X.dir, ioDir, false);
|
59 | }
|
60 |
|
61 | public static File loadSnippetAsMainJava(String snippetID) throws IOException {
|
62 | File srcDir = TempDirMaker.make();
|
63 | saveTextFile(new File(srcDir, "main.java").getPath(), loadSnippet(snippetID));
|
64 | return srcDir;
|
65 | }
|
66 |
|
67 | public static File loadSnippetAsMainJavaVerified(String snippetID, String hash) throws IOException {
|
68 | File srcDir = TempDirMaker.make();
|
69 | saveTextFile(new File(srcDir, "main.java").getPath(), loadSnippetVerified(snippetID, hash));
|
70 | return srcDir;
|
71 | }
|
72 |
|
73 | private static Input expandProgram(Input x) throws IOException {
|
74 | return runJavaX2_on_snippet(mainAdder, mainAdderHash, x, true);
|
75 | }
|
76 |
|
77 | private static boolean isCompilable(Input x) throws IOException {
|
78 | Input output = runJavaX2_on_snippet(isCompilable, isCompilableHash, x, true);
|
79 | return output.hasFile("is-compilable");
|
80 | }
|
81 |
|
82 | private static Input runJavaX2_on_snippet(String snippetID, String hash, Input x, boolean silent) throws IOException {
|
83 | File srcDir = loadSnippetAsMainJavaVerified(snippetID, hash);
|
84 | return runJavaX(new Input(srcDir), x, silent);
|
85 | }
|
86 |
|
87 | private static Input runJavaX(Input originalSrcDir, Input originalInput, boolean silent) throws IOException {
|
88 | File ioBaseDir = TempDirMaker.make();
|
89 | File srcDir = new File(ioBaseDir, "src");
|
90 | File inputDir = new File(ioBaseDir, "input");
|
91 | Input input = new Input(inputDir);
|
92 | File outputDir = new File(ioBaseDir, "output");
|
93 | Input output = new Input(outputDir);
|
94 | copyInput(originalSrcDir, new Input(srcDir));
|
95 | copyInput(originalInput, input);
|
96 | javax2(srcDir, ioBaseDir, silent);
|
97 | return output;
|
98 | }
|
99 |
|
100 | private static void copyInput(Input src, Input dst) throws IOException {
|
101 | copyDirectory(src.dir, dst.dir);
|
102 | }
|
103 |
|
104 | private static Input programToInput(File srcDir) {
|
105 | return new Input(srcDir);
|
106 | }
|
107 |
|
108 | static class Input {
|
109 | File dir;
|
110 |
|
111 | public Input(File dir) {
|
112 | this.dir = dir;
|
113 | }
|
114 |
|
115 | public boolean hasFile(String name) {
|
116 | return new File(dir, name).exists();
|
117 | }
|
118 | }
|
119 |
|
120 | public static void copyDirectory(File src, File dst) throws IOException {
|
121 | if (verbose) System.out.println("Copying " + src.getAbsolutePath() + " to " + dst.getAbsolutePath());
|
122 | dst.mkdirs();
|
123 | File[] files = src.listFiles();
|
124 | if (files == null) return;
|
125 | for (File file : files) {
|
126 | File dst1 = new File(dst, file.getName());
|
127 | if (file.isDirectory())
|
128 | copyDirectory(file, dst1);
|
129 | else {
|
130 | if (verbose) System.out.println("Copying " + file.getAbsolutePath() + " to " + dst1.getAbsolutePath());
|
131 | copy(file, dst1);
|
132 | }
|
133 | }
|
134 | }
|
135 |
|
136 | /** Quickly copy a file without a progress bar or any other fancy GUI... :) */
|
137 | public static void copy(File src, File dest) throws IOException {
|
138 | FileInputStream inputStream = new FileInputStream(src);
|
139 | FileOutputStream outputStream = new FileOutputStream(dest);
|
140 | try {
|
141 | copy(inputStream, outputStream);
|
142 | inputStream.close();
|
143 | } finally {
|
144 | outputStream.close();
|
145 | }
|
146 | }
|
147 |
|
148 | public static void copy(InputStream in, OutputStream out) throws IOException {
|
149 | byte[] buf = new byte[65536];
|
150 | while (true) {
|
151 | int n = in.read(buf);
|
152 | if (n <= 0) return;
|
153 | out.write(buf, 0, n);
|
154 | }
|
155 | }
|
156 |
|
157 | /** writes safely (to temp file, then rename) */
|
158 | public static void saveTextFile(String fileName, String contents) throws IOException {
|
159 | File file = new File(fileName);
|
160 | File parentFile = file.getParentFile();
|
161 | if (parentFile != null)
|
162 | parentFile.mkdirs();
|
163 | String tempFileName = fileName + "_temp";
|
164 | FileOutputStream fileOutputStream = new FileOutputStream(tempFileName);
|
165 | OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, charsetForTextFiles);
|
166 | PrintWriter printWriter = new PrintWriter(outputStreamWriter);
|
167 | printWriter.print(contents);
|
168 | printWriter.close();
|
169 | if (file.exists() && !file.delete())
|
170 | throw new IOException("Can't delete " + fileName);
|
171 |
|
172 | if (!new File(tempFileName).renameTo(file))
|
173 | throw new IOException("Can't rename " + tempFileName + " to " + fileName);
|
174 | }
|
175 |
|
176 | public static String loadTextFile(String fileName, String defaultContents) throws IOException {
|
177 | if (!new File(fileName).exists())
|
178 | return defaultContents;
|
179 |
|
180 | FileInputStream fileInputStream = new FileInputStream(fileName);
|
181 | InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, charsetForTextFiles);
|
182 | return loadTextFile(inputStreamReader);
|
183 | }
|
184 |
|
185 | public static String loadTextFile(Reader reader) throws IOException {
|
186 | StringBuilder builder = new StringBuilder();
|
187 | try {
|
188 | BufferedReader bufferedReader = new BufferedReader(reader);
|
189 | String line;
|
190 | while ((line = bufferedReader.readLine()) != null)
|
191 | builder.append(line).append('\n');
|
192 | } finally {
|
193 | reader.close();
|
194 | }
|
195 | return builder.length() == 0 ? "" : builder.substring(0, builder.length()-1);
|
196 | }
|
197 |
|
198 | static class DiskSnippetCache {
|
199 | final File dir;
|
200 |
|
201 | public DiskSnippetCache(File dir) {
|
202 | this.dir = dir;
|
203 | dir.mkdirs();
|
204 | }
|
205 |
|
206 | public synchronized String get(long snippetID) throws IOException {
|
207 | return loadTextFile(getFile(snippetID).getPath(), null);
|
208 | }
|
209 |
|
210 | private File getFile(long snippetID) {
|
211 | return new File(dir, "" + snippetID);
|
212 | }
|
213 |
|
214 | public synchronized void put(long snippetID, String snippet) throws IOException {
|
215 | saveTextFile(getFile(snippetID).getPath(), snippet);
|
216 | }
|
217 |
|
218 | public File getDir() {
|
219 | return dir;
|
220 | }
|
221 | }
|
222 |
|
223 | public static DiskSnippetCache getSnippetCache() {
|
224 | return new DiskSnippetCache(new File(System.getProperty("user.home"), ".tinybrain/snippet-cache"));
|
225 | }
|
226 |
|
227 | public static String loadSnippetVerified(String snippetID, String hash) throws IOException {
|
228 | String text = loadSnippet(snippetID);
|
229 | String realHash = getHash(text.getBytes("UTF-8"));
|
230 | if (!realHash.equals(hash)) {
|
231 | String msg;
|
232 | if (hash.isEmpty())
|
233 | msg = "Here's your hash for " + snippetID + ", please put in your program: " + realHash;
|
234 | else
|
235 | msg = "Hash mismatch for " + snippetID + ": " + realHash + " (new) vs " + hash + " - has tinybrain.de been hacked??";
|
236 | throw new RuntimeException(msg);
|
237 | }
|
238 | return text;
|
239 | }
|
240 |
|
241 | public static String getHash(byte[] data) {
|
242 | return bytesToHex(getFullFingerprint(data));
|
243 | }
|
244 |
|
245 | public static byte[] getFullFingerprint(byte[] data) {
|
246 | try {
|
247 | return MessageDigest.getInstance("MD5").digest(data);
|
248 | } catch (NoSuchAlgorithmException e) {
|
249 | throw new RuntimeException(e);
|
250 | }
|
251 | }
|
252 |
|
253 | public static String bytesToHex(byte[] bytes) {
|
254 | return bytesToHex(bytes, 0, bytes.length);
|
255 | }
|
256 |
|
257 | public static String bytesToHex(byte[] bytes, int ofs, int len) {
|
258 | StringBuilder stringBuilder = new StringBuilder(len*2);
|
259 | for (int i = 0; i < len; i++) {
|
260 | String s = "0" + Integer.toHexString(bytes[ofs+i]);
|
261 | stringBuilder.append(s.substring(s.length()-2, s.length()));
|
262 | }
|
263 | return stringBuilder.toString();
|
264 | }
|
265 |
|
266 | public static String loadSnippet(String snippetID) throws IOException {
|
267 | return loadSnippet(parseSnippetID(snippetID));
|
268 | }
|
269 |
|
270 | public static long parseSnippetID(String snippetID) {
|
271 | return Long.parseLong(shortenSnippetID(snippetID));
|
272 | }
|
273 |
|
274 | private static String shortenSnippetID(String snippetID) {
|
275 | if (snippetID.startsWith("#"))
|
276 | snippetID = snippetID.substring(1);
|
277 | String httpBlaBla = "http://tinybrain.de/";
|
278 | if (snippetID.startsWith(httpBlaBla))
|
279 | snippetID = snippetID.substring(httpBlaBla.length());
|
280 | return snippetID;
|
281 | }
|
282 |
|
283 | public static boolean isSnippetID(String snippetID) {
|
284 | snippetID = shortenSnippetID(snippetID);
|
285 | return isInteger(snippetID) && Long.parseLong(snippetID) != 0;
|
286 | }
|
287 |
|
288 | public static boolean isInteger(String s) {
|
289 | return Pattern.matches("\\-?\\d+", s);
|
290 | }
|
291 |
|
292 | static boolean preferCached = true;
|
293 |
|
294 | public static String loadSnippet(long snippetID) throws IOException {
|
295 | if (preferCached) {
|
296 | String text = getSnippetCache().get(snippetID);
|
297 | if (text != null)
|
298 | return text;
|
299 | }
|
300 |
|
301 | String text;
|
302 | try {
|
303 | URL url = new URL("http://tinybrain.de:8080/getraw.php?id=" + snippetID);
|
304 | text = loadPage(url);
|
305 | } catch (FileNotFoundException e) {
|
306 | throw new IOException("Snippet #" + snippetID + " not found or not public");
|
307 | }
|
308 |
|
309 | try {
|
310 | getSnippetCache().put(snippetID, text);
|
311 | } catch (IOException e) {
|
312 | System.err.println("Minor warning: Couldn't save snippet to cache (" + getSnippetCache().getDir() + ")");
|
313 | }
|
314 |
|
315 | return text;
|
316 | }
|
317 |
|
318 | private static String loadPage(URL url) throws IOException {
|
319 | System.out.println("Loading: " + url.toExternalForm());
|
320 | URLConnection con = url.openConnection();
|
321 | return loadPage(con, url);
|
322 | }
|
323 |
|
324 | public static String loadPage(URLConnection con, URL url) throws IOException {
|
325 | String contentType = con.getContentType();
|
326 | if (contentType == null)
|
327 | throw new IOException("Page could not be read: " + url);
|
328 | //Log.info("Content-Type: " + contentType);
|
329 | String charset = guessCharset(contentType);
|
330 | Reader r = new InputStreamReader(con.getInputStream(), charset);
|
331 | StringBuilder buf = new StringBuilder();
|
332 | while (true) {
|
333 | int ch = r.read();
|
334 | if (ch < 0)
|
335 | break;
|
336 | //Log.info("Chars read: " + buf.length());
|
337 | buf.append((char) ch);
|
338 | }
|
339 | return buf.toString();
|
340 | }
|
341 |
|
342 | public static String guessCharset(String contentType) {
|
343 | Pattern p = Pattern.compile("text/html;\\s+charset=([^\\s]+)\\s*");
|
344 | Matcher m = p.matcher(contentType);
|
345 | /* If Content-Type doesn't match this pre-conception, choose default and hope for the best. */
|
346 | return m.matches() ? m.group(1) : "ISO-8859-1";
|
347 | }
|
348 |
|
349 | public static void javax2(File srcDir, File ioBaseDir, boolean silent) throws IOException {
|
350 | List<File> sources = new ArrayList<File>();
|
351 | if (verbose) System.out.println("Scanning for sources in " + srcDir.getPath());
|
352 | scanForSources(srcDir, sources, true);
|
353 | if (sources.isEmpty()) {
|
354 | System.out.println("No sources found");
|
355 | return;
|
356 | }
|
357 | File optionsFile = File.createTempFile("javax", "");
|
358 | File classesDir = TempDirMaker.make();
|
359 | if (verbose) System.out.println("Compiling " + sources.size() + " source(s) to " + classesDir.getPath());
|
360 | String options = "-d " + bashQuote(classesDir.getPath());
|
361 | writeOptions(sources, optionsFile, options);
|
362 | classesDir.mkdirs();
|
363 | invokeJavac(optionsFile);
|
364 | if (verbose) System.out.println("Running program (class main.java)\n");
|
365 | runProgram(classesDir, ioBaseDir, silent);
|
366 | }
|
367 |
|
368 | private static void runProgram(File classesDir, File ioBaseDir, boolean silent) throws IOException {
|
369 | boolean echoOK = false;
|
370 | String bashCmd = "(cd " + bashQuote(ioBaseDir.getAbsolutePath()) + " && (java -cp "
|
371 | + bashQuote(classesDir.getAbsolutePath()) + " main" + (echoOK ? "; echo ok" : "") + "))";
|
372 | if (verbose) System.out.println(bashCmd);
|
373 | String output = backtick(bashCmd);
|
374 | if (!silent)
|
375 | System.out.println(output);
|
376 | }
|
377 |
|
378 | private static void invokeJavac(File optionsFile) throws IOException {
|
379 | String javacOutput = backtick("javac " + bashQuote("@" + optionsFile.getPath()));
|
380 | if (verbose) System.out.println(javacOutput);
|
381 | }
|
382 |
|
383 | private static void writeOptions(List<File> sources, File sourcesFile, String moreOptions) throws IOException {
|
384 | FileWriter writer = new FileWriter(sourcesFile);
|
385 | for (File source : sources)
|
386 | writer.write(bashQuote(source.getPath()) + " ");
|
387 | writer.write(moreOptions);
|
388 | writer.close();
|
389 | }
|
390 |
|
391 | private static void scanForSources(File source, List<File> sources, boolean topLevel) {
|
392 | if (source.isFile() && source.getName().endsWith(".java"))
|
393 | sources.add(source);
|
394 | else if (source.isDirectory() && !isSkippedDirectoryName(source.getName(), topLevel)) {
|
395 | File[] files = source.listFiles();
|
396 | for (File file : files)
|
397 | scanForSources(file, sources, false);
|
398 | }
|
399 | }
|
400 |
|
401 | private static boolean isSkippedDirectoryName(String name, boolean topLevel) {
|
402 | if (topLevel) return false; // input or output ok as highest directory (intentionally specified by user, not just found by a directory scan in which case we probably don't want it. it's more like heuristics actually.)
|
403 | return name.equalsIgnoreCase("input") || name.equalsIgnoreCase("output");
|
404 | }
|
405 |
|
406 | public static String backtick(String cmd) throws IOException {
|
407 | File outFile = File.createTempFile("_backtick", "");
|
408 | File scriptFile = File.createTempFile("_backtick", "");
|
409 |
|
410 | String command = cmd + ">" + bashQuote(outFile.getPath()) + " 2>&1";
|
411 | //Log.info("[Backtick] " + command);
|
412 | try {
|
413 | saveTextFile(scriptFile.getPath(), command);
|
414 | String[] command2 = {"/bin/bash", scriptFile.getPath() };
|
415 | Process process = Runtime.getRuntime().exec(command2);
|
416 | try {
|
417 | process.waitFor();
|
418 | } catch (InterruptedException e) {
|
419 | throw new RuntimeException(e);
|
420 | }
|
421 | int value = process.exitValue();
|
422 | //Log.info("exit value: " + value);
|
423 | return loadTextFile(outFile.getPath(), "");
|
424 | } finally {
|
425 | scriptFile.delete();
|
426 | }
|
427 | }
|
428 |
|
429 | /** possibly improvable */
|
430 | public static String bashQuote(String text) {
|
431 | if (text == null) return null;
|
432 | return "\"" + text
|
433 | .replace("\\", "\\\\")
|
434 | .replace("\"", "\\\"")
|
435 | .replace("\n", "\\n")
|
436 | .replace("\r", "\\r") + "\"";
|
437 | }
|
438 |
|
439 | public final static String charsetForTextFiles = "UTF8";
|
440 |
|
441 | public static class TempDirMaker {
|
442 | static long lastValue;
|
443 |
|
444 | public static File make() {
|
445 | File dir = new File(System.getProperty("user.home"), ".javax/" + newValue());
|
446 | dir.mkdirs();
|
447 | return dir;
|
448 | }
|
449 |
|
450 | private static long newValue() {
|
451 | long value;
|
452 | do
|
453 | value = System.currentTimeMillis();
|
454 | while (value == lastValue);
|
455 | lastValue = value;
|
456 | return value;
|
457 | }
|
458 | }
|
459 | }
|