package net.mcforkage.ant; import immibis.bon.com.immibis.json.JsonReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Task; import bytecode.BaseStreamingZipProcessor; public class DownloadLibrariesTask extends Task { private File jsonfile, libsdir, nativesdir; public void setJsonfile(File f) { jsonfile = f; } public void setLibsdir(File f) { libsdir = f; } public void setNativesdir(File f) { nativesdir = f; } private static class PendingDownload { URL url; File file; Object nativesJson; public PendingDownload(URL url, File file, Object nativesJson) { this.url = url; this.file = file; this.nativesJson = nativesJson; } } @Override public void execute() throws BuildException { if(jsonfile == null) throw new BuildException("jsonfile not specified"); if(libsdir == null) throw new BuildException("libsdir not specified"); if(nativesdir == null) throw new BuildException("nativesdir not specified"); String osType = "unknown"; { String osname = System.getProperty("os.name").toLowerCase(); if(osname.contains("linux") || osname.contains("unix")) osType = "linux"; if(osname.contains("win")) osType = "windows"; if(osname.contains("mac")) osType = "mac"; } String arch = System.getProperty("os.arch").contains("64") ? "64" : "32"; Map json; try (FileReader fr = new FileReader(jsonfile)) { json = (Map)JsonReader.readJSON(fr); } catch(IOException e) { throw new BuildException("Failed to read or parse "+jsonfile, e); } List<PendingDownload> allDownloads = new ArrayList<DownloadLibrariesTask.PendingDownload>(); for(Object libraryObject : (List<?>)json.get("libraries")) allDownloads.addAll(getDownloadsForLibrary(libraryObject, osType, arch)); List<PendingDownload> toDownload = new ArrayList<PendingDownload>(allDownloads); for(Iterator<PendingDownload> it = toDownload.iterator(); it.hasNext();) if(it.next().file.exists()) it.remove(); for(int k = 0; k < toDownload.size(); k++) { PendingDownload download = toDownload.get(k); System.out.println("Downloading "+(k+1)+"/"+toDownload.size()+": "+download.file.getName()); download(download); } for(PendingDownload download : allDownloads) { if(download.nativesJson != null) { extractNatives(download.file, nativesdir); } } } private List<PendingDownload> getDownloadsForLibrary(Object libraryObject, String osType, String arch) { Map<String, Object> library = (Map<String, Object>)libraryObject; Object nameObject = library.get("name"); Object urlObject = library.get("url"); Object childrenObject = library.get("children"); Object rulesObject = library.get("rules"); Object nativesObject = library.get("natives"); String name = (String)nameObject; if(rulesObject != null) { if(!checkRules((List<?>)rulesObject, osType)) { return Collections.emptyList(); } } List<PendingDownload> rv = new ArrayList<>(); List<String> suffixes = new ArrayList<>(); if(nativesObject == null) suffixes.add(""); else { String nativeSuffix = (String)((Map<String, ?>)nativesObject).get(osType); if(nativeSuffix == null) throw new BuildException("natives library "+name+" has no native suffix specified"); suffixes.add("-" + (String)nativeSuffix); } if(childrenObject != null) for(String o : (List<String>)childrenObject) suffixes.add("-" + o); String baseURL = (urlObject != null ? (String)urlObject + "/" : "https://libraries.minecraft.net/"); if(baseURL.endsWith("//")) baseURL = baseURL.substring(0, baseURL.length() - 1); String[] nameParts = name.split(":"); if(nameParts.length != 3) throw new BuildException("malformed library name: "+name); for(String suffix : suffixes) { String fileName = nameParts[1] + "-" + nameParts[2] + suffix + ".jar"; fileName = fileName.replace("${arch}", arch); File file = new File(libsdir, fileName); if(!file.getParentFile().getAbsolutePath().equals(libsdir.getAbsolutePath())) throw new SecurityException("Filename contains separator. Filename is: "+fileName); String url = baseURL + nameParts[0].replace(".", "/") + "/" + nameParts[1] + "/" + nameParts[2] + "/" + fileName; try { rv.add(new PendingDownload(new URL(url), file, (suffix.equals(suffixes.get(0)) ? nativesObject : null))); } catch (MalformedURLException e) { throw new BuildException(e); } } return rv; } private static void extractNatives(File zipFile, File nativesDir) throws BuildException { //System.out.println("extracting natives: "+zipFile.getName()); try (ZipInputStream zin = new ZipInputStream(new FileInputStream(zipFile))) { ZipEntry ze; while((ze = zin.getNextEntry()) != null) { if(ze.getName().endsWith("/") || ze.getName().startsWith("META-INF/")) { zin.closeEntry(); continue; } File outFile = new File(nativesDir, ze.getName()); if(!outFile.exists()) { if(!outFile.getParentFile().getAbsolutePath().equals(nativesDir.getAbsolutePath())) throw new SecurityException("filename contains separator: "+ze.getName()); try (FileOutputStream fout = new FileOutputStream(outFile)) { BaseStreamingZipProcessor.copyResource(zin, fout); } } zin.closeEntry(); } } catch(IOException e) { throw new BuildException("Failed to extract natives from "+zipFile+" to "+nativesDir, e); } } private static void download(PendingDownload info) throws BuildException { try { HttpURLConnection conn = (HttpURLConnection)info.url.openConnection(); conn.setRequestProperty("User-Agent", "TotallyNotJavaMasqueradingAsRandomStuffBecauseForSomeReasonJavaUserAgentsAreBlacklistedButOnlyFromSomeRepositories/1.0"); File tempfile = new File(info.file.getParentFile(), "temp-downloading"); try (InputStream downloadStream = conn.getInputStream()) { try (OutputStream fileStream = new FileOutputStream(tempfile)) { BaseStreamingZipProcessor.copyResource(downloadStream, fileStream); } } // Thanks Oracle. for(int k = 0; k < 5; k++) { info.file.delete(); if(info.file.exists()) Thread.sleep(200); else break; } for(int k = 0; k < 5; k++) { if(tempfile.renameTo(info.file)) break; } } catch(IOException | InterruptedException e) { throw new BuildException("Failed to download "+info.url, e); } } /** Returns true if this library is allowed. */ @SuppressWarnings("unchecked") private static boolean checkRules(List<?> rules, String osType) throws BuildException { boolean allowed = false; for(Object ruleObject : rules) { if(!(ruleObject instanceof Map)) throw new BuildException("malformed dev.json"); Map<String, ?> rule = (Map<String, ?>)ruleObject; boolean ruleAction; if("allow".equals(rule.get("action"))) ruleAction = true; else if("disallow".equals(rule.get("action"))) ruleAction = false; else throw new BuildException("malformed dev.json"); Map.Entry<String, ?> condition = null; for(Map.Entry<String, ?> entry : rule.entrySet()) { if(entry.getKey().equals("action")) continue; else if(condition == null) condition = entry; else throw new BuildException("can't handle rule with more than one condition in dev.json: conditions are "+entry.getKey()+" and "+condition.getKey()); } if(condition == null) allowed = ruleAction; else if(condition.getKey().equals("os")) { if(!(condition.getValue() instanceof Map)) throw new BuildException("malformed dev.json"); Map<String, ?> attrs = (Map<String, ?>)condition.getValue(); if(attrs.size() != 1 || !attrs.containsKey("name")) throw new BuildException("can't handle os condition: "+attrs); if(osType.equals(attrs.get("name"))) allowed = ruleAction; } else throw new BuildException("can't handle unknown rule condition "+condition.getKey()); } return allowed; } }