1 /* 2 * This file is part of Arduino. 3 * 4 * Copyright 2014 Arduino LLC (http://www.arduino.cc/) 5 * 6 * Arduino is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, write to the Free Software 18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 19 * 20 * As a special exception, you may use this file as part of a free software 21 * library without restriction. Specifically, if other files instantiate 22 * templates or use macros or inline functions from this file, or you compile 23 * this file and link it with other files to produce an executable, this 24 * file does not by itself cause the resulting executable to be covered by 25 * the GNU General Public License. This exception does not however 26 * invalidate any other reasons why the executable file might be covered by 27 * the GNU General Public License. 28 */ 29 30 package cc.arduino.contributions.packages; 31 32 import cc.arduino.Constants; 33 import cc.arduino.contributions.DownloadableContribution; 34 import cc.arduino.contributions.DownloadableContributionBuiltInAtTheBottomComparator; 35 import cc.arduino.contributions.SignatureVerificationFailedException; 36 import cc.arduino.contributions.SignatureVerifier; 37 import cc.arduino.contributions.filters.BuiltInPredicate; 38 import cc.arduino.contributions.filters.InstalledPredicate; 39 40 import com.fasterxml.jackson.core.JsonProcessingException; 41 import com.fasterxml.jackson.databind.DeserializationFeature; 42 import com.fasterxml.jackson.databind.ObjectMapper; 43 import com.fasterxml.jackson.module.mrbean.MrBeanModule; 44 import org.apache.commons.compress.utils.IOUtils; 45 import processing.app.I18n; 46 import processing.app.Platform; 47 import processing.app.PreferencesData; 48 import processing.app.debug.TargetPackage; 49 import processing.app.debug.TargetPlatform; 50 import processing.app.debug.TargetPlatformException; 51 import processing.app.helpers.FileUtils; 52 import processing.app.helpers.PreferencesMap; 53 54 import java.io.File; 55 import java.io.FileInputStream; 56 import java.io.IOException; 57 import java.io.InputStream; 58 import java.util.*; 59 import java.util.stream.Collectors; 60 61 import static processing.app.I18n.tr; 62 import static processing.app.helpers.filefilters.OnlyDirs.ONLY_DIRS; 63 64 public class ContributionsIndexer { 65 66 private final File packagesFolder; 67 private final File stagingFolder; 68 private final File preferencesFolder; 69 private final File builtInHardwareFolder; 70 private final Platform platform; 71 private final SignatureVerifier signatureVerifier; 72 private final ContributionsIndex index; 73 ContributionsIndexer(File preferencesFolder, File builtInHardwareFolder, Platform platform, SignatureVerifier signatureVerifier)74 public ContributionsIndexer(File preferencesFolder, File builtInHardwareFolder, Platform platform, SignatureVerifier signatureVerifier) { 75 this.preferencesFolder = preferencesFolder; 76 this.builtInHardwareFolder = builtInHardwareFolder; 77 this.platform = platform; 78 this.signatureVerifier = signatureVerifier; 79 index = new EmptyContributionIndex(); 80 packagesFolder = new File(preferencesFolder, "packages"); 81 stagingFolder = new File(preferencesFolder, "staging" + File.separator + "packages"); 82 } 83 parseIndex()84 public void parseIndex() throws Exception { 85 // Read bundled index... 86 File bundledIndexFile = new File(builtInHardwareFolder, Constants.BUNDLED_INDEX_FILE_NAME); 87 mergeContributions(bundledIndexFile); 88 89 // ...and overlay the default index if present 90 File defaultIndexFile = getIndexFile(Constants.DEFAULT_INDEX_FILE_NAME); 91 if (defaultIndexFile.exists()) { 92 // Check main index signature 93 if (!PreferencesData.getBoolean("allow_insecure_packages") && !signatureVerifier.isSigned(defaultIndexFile)) { 94 throw new SignatureVerificationFailedException(Constants.DEFAULT_INDEX_FILE_NAME); 95 } 96 97 mergeContributions(defaultIndexFile); 98 } 99 100 // Set main and bundled indexes as trusted 101 index.getPackages().forEach(pack -> pack.setTrusted(true)); 102 103 // Overlay 3rd party indexes 104 File[] indexFiles = preferencesFolder.listFiles(new TestPackageIndexFilenameFilter(new PackageIndexFilenameFilter(Constants.DEFAULT_INDEX_FILE_NAME))); 105 106 for (File indexFile : indexFiles) { 107 try { 108 mergeContributions(indexFile); 109 } catch (JsonProcessingException e) { 110 System.err.println(I18n.format(tr("Skipping contributed index file {0}, parsing error occured:"), indexFile)); 111 System.err.println(e); 112 } 113 } 114 115 // Fill tools and toolsDependency cross references 116 List<ContributedPackage> packages = index.getPackages(); 117 Collection<ContributedPackage> packagesWithTools = packages.stream() 118 .filter(input -> input.getTools() != null && !input.getTools().isEmpty()) 119 .collect(Collectors.toList()); 120 121 for (ContributedPackage pack : packages) { 122 // Fill references to package in tools 123 for (ContributedTool tool : pack.getTools()) { 124 tool.setPackage(pack); 125 } 126 127 for (ContributedPlatform plat : pack.getPlatforms()) { 128 // Set a reference to parent packages 129 plat.setParentPackage(pack); 130 131 // Resolve tools dependencies (works also as a check for file integrity) 132 plat.resolveToolsDependencies(packagesWithTools); 133 } 134 } 135 136 index.fillCategories(); 137 } 138 mergeContributions(File indexFile)139 private void mergeContributions(File indexFile) throws IOException { 140 if (!indexFile.exists()) 141 return; 142 143 ContributionsIndex contributionsIndex = parseIndex(indexFile); 144 boolean signed = signatureVerifier.isSigned(indexFile); 145 boolean trustall = PreferencesData.getBoolean(Constants.PREF_CONTRIBUTIONS_TRUST_ALL); 146 147 for (ContributedPackage contributedPackage : contributionsIndex.getPackages()) { 148 contributedPackage.setTrusted(signed || trustall); 149 if (!contributedPackage.isTrusted()) { 150 for (ContributedPlatform contributedPlatform : contributedPackage.getPlatforms()) { 151 contributedPlatform.setCategory("Contributed"); 152 } 153 } 154 155 ContributedPackage targetPackage = index.getPackage(contributedPackage.getName()); 156 157 if (targetPackage == null) { 158 index.getPackages().add(contributedPackage); 159 } else { 160 if (contributedPackage.isTrusted() || !isPackageNameProtected(contributedPackage)) { 161 if (isPackageNameProtected(contributedPackage) && trustall) { 162 System.err.println(I18n.format(tr("Warning: forced trusting untrusted contributions"))); 163 } 164 List<ContributedPlatform> platforms = contributedPackage.getPlatforms(); 165 if (platforms == null) { 166 platforms = new LinkedList<>(); 167 } 168 for (ContributedPlatform contributedPlatform : platforms) { 169 ContributedPlatform plat = targetPackage.findPlatform(contributedPlatform.getArchitecture(), contributedPlatform.getVersion()); 170 if (plat != null) { 171 targetPackage.getPlatforms().remove(plat); 172 } 173 targetPackage.getPlatforms().add(contributedPlatform); 174 } 175 List<ContributedTool> tools = contributedPackage.getTools(); 176 if (tools == null) { 177 tools = new LinkedList<>(); 178 } 179 for (ContributedTool contributedTool : tools) { 180 ContributedTool tool = targetPackage.findTool(contributedTool.getName(), contributedTool.getVersion()); 181 if (tool != null) { 182 targetPackage.getTools().remove(tool); 183 } 184 targetPackage.getTools().add(contributedTool); 185 } 186 } 187 } 188 } 189 } 190 isPackageNameProtected(ContributedPackage contributedPackage)191 private boolean isPackageNameProtected(ContributedPackage contributedPackage) { 192 return Constants.PROTECTED_PACKAGE_NAMES.contains(contributedPackage.getName()); 193 } 194 parseIndex(File indexFile)195 private ContributionsIndex parseIndex(File indexFile) throws IOException { 196 InputStream inputStream = null; 197 try { 198 inputStream = new FileInputStream(indexFile); 199 ObjectMapper mapper = new ObjectMapper(); 200 mapper.registerModule(new MrBeanModule()); 201 mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); 202 mapper.configure(DeserializationFeature.EAGER_DESERIALIZER_FETCH, true); 203 mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 204 return mapper.readValue(inputStream, ContributionsIndex.class); 205 } finally { 206 IOUtils.closeQuietly(inputStream); 207 } 208 } 209 syncWithFilesystem()210 public void syncWithFilesystem() throws IOException { 211 syncBuiltInHardware(); 212 213 syncLocalPackages(); 214 } 215 syncBuiltInHardware()216 private void syncBuiltInHardware() throws IOException { 217 if (index == null) { 218 return; 219 } 220 for (File folder : builtInHardwareFolder.listFiles(ONLY_DIRS)) { 221 ContributedPackage pack = index.findPackage(folder.getName()); 222 if (pack == null) 223 continue; 224 syncBuiltInPackageWithFilesystem(pack, folder); 225 226 File toolsFolder = new File(builtInHardwareFolder, "tools"); 227 if (!toolsFolder.isDirectory()) 228 continue; 229 230 for (File toolFolder : toolsFolder.listFiles(ONLY_DIRS)) { 231 232 // builtin_tools_versions.txt contains tools versions in the format: 233 // "PACKAGER.TOOL_NAME=TOOL_VERSION" 234 // for example: 235 // "arduino.avrdude=6.0.1-arduino5" 236 237 File versionsFile = new File(toolFolder, "builtin_tools_versions.txt"); 238 if (!versionsFile.isFile()) 239 continue; 240 PreferencesMap toolsVersion = new PreferencesMap(versionsFile).subTree(pack.getName()); 241 for (String name : toolsVersion.keySet()) { 242 String version = toolsVersion.get(name); 243 DownloadableContribution tool = syncToolWithFilesystem(pack, toolFolder, name, version); 244 if (tool != null) 245 tool.setReadOnly(true); 246 } 247 } 248 } 249 } 250 syncBuiltInPackageWithFilesystem(ContributedPackage pack, File hardwareFolder)251 private void syncBuiltInPackageWithFilesystem(ContributedPackage pack, File hardwareFolder) throws IOException { 252 // Scan all hardware folders and mark as installed all the tools found. 253 for (File platformFolder : hardwareFolder.listFiles(ONLY_DIRS)) { 254 File platformTxt = new File(platformFolder, "platform.txt"); 255 String version = new PreferencesMap(platformTxt).get("version"); 256 ContributedPlatform p = syncHardwareWithFilesystem(pack, platformFolder, platformFolder.getName(), version); 257 if (p != null) { 258 p.setReadOnly(true); 259 } 260 } 261 } 262 syncLocalPackages()263 private void syncLocalPackages() { 264 if (!packagesFolder.isDirectory()) { 265 return; 266 } 267 268 if (index == null) { 269 return; 270 } 271 272 // Scan all hardware folders and mark as installed all the 273 // platforms found. 274 for (File folder : packagesFolder.listFiles(ONLY_DIRS)) { 275 ContributedPackage pack = index.findPackage(folder.getName()); 276 if (pack != null) { 277 syncPackageWithFilesystem(pack, folder); 278 } 279 } 280 } 281 syncPackageWithFilesystem(ContributedPackage pack, File root)282 private void syncPackageWithFilesystem(ContributedPackage pack, File root) { 283 // Scan all hardware folders and mark as installed all the tools found. 284 File hardwareFolder = new File(root, "hardware"); 285 if (hardwareFolder.isDirectory()) { 286 for (File platformFolder : hardwareFolder.listFiles(ONLY_DIRS)) { 287 for (File versionFolder : platformFolder.listFiles(ONLY_DIRS)) { 288 syncHardwareWithFilesystem(pack, versionFolder, platformFolder.getName(), versionFolder.getName()); 289 } 290 } 291 } 292 293 // Scan all tools folders and mark as installed all the tools found. 294 File toolsFolder = new File(root, "tools"); 295 if (toolsFolder.isDirectory()) { 296 for (File toolFolder : toolsFolder.listFiles(ONLY_DIRS)) { 297 for (File versionFolder : toolFolder.listFiles(ONLY_DIRS)) { 298 syncToolWithFilesystem(pack, versionFolder, toolFolder.getName(), versionFolder.getName()); 299 } 300 } 301 } 302 } 303 syncToolWithFilesystem(ContributedPackage pack, File installationFolder, String toolName, String version)304 private DownloadableContribution syncToolWithFilesystem(ContributedPackage pack, File installationFolder, String toolName, String version) { 305 ContributedTool tool = pack.findTool(toolName, version); 306 if (tool == null) { 307 tool = pack.findResolvedTool(toolName, version); 308 } 309 if (tool == null) { 310 return null; 311 } 312 DownloadableContribution contrib = tool.getDownloadableContribution(platform); 313 if (contrib == null) { 314 System.err.println(tool + " seems to have no downloadable contributions for your operating system, but it is installed in\n" + installationFolder); 315 return null; 316 } 317 contrib.setInstalled(true); 318 contrib.setInstalledFolder(installationFolder); 319 contrib.setReadOnly(false); 320 return contrib; 321 } 322 syncHardwareWithFilesystem(ContributedPackage pack, File installationFolder, String architecture, String version)323 private ContributedPlatform syncHardwareWithFilesystem(ContributedPackage pack, File installationFolder, String architecture, String version) { 324 ContributedPlatform p = pack.findPlatform(architecture, version); 325 if (p != null) { 326 p.setInstalled(true); 327 p.setReadOnly(false); 328 p.setInstalledFolder(installationFolder); 329 } 330 return p; 331 } 332 333 @Override toString()334 public String toString() { 335 return index.toString(); 336 } 337 createTargetPackages()338 public List<TargetPackage> createTargetPackages() { 339 List<TargetPackage> packages = new ArrayList<>(); 340 341 if (index == null) { 342 return packages; 343 } 344 345 for (ContributedPackage aPackage : index.getPackages()) { 346 ContributedTargetPackage targetPackage = new ContributedTargetPackage(aPackage.getName()); 347 348 List<ContributedPlatform> platforms = aPackage.getPlatforms().stream().filter(new InstalledPredicate()).collect(Collectors.toList()); 349 Collections.sort(platforms, new DownloadableContributionBuiltInAtTheBottomComparator()); 350 351 for (ContributedPlatform p : platforms) { 352 String arch = p.getArchitecture(); 353 File folder = p.getInstalledFolder(); 354 355 try { 356 TargetPlatform targetPlatform = new ContributedTargetPlatform(arch, folder, targetPackage); 357 if (!targetPackage.hasPlatform(targetPlatform)) { 358 targetPackage.addPlatform(targetPlatform); 359 } 360 } catch (TargetPlatformException e) { 361 System.err.println(e.getMessage()); 362 } 363 } 364 365 if (targetPackage.hasPlatforms()) { 366 packages.add(targetPackage); 367 } 368 } 369 370 Collections.sort(packages, (package1, package2) -> { 371 assert package1.getId() != null && package2.getId() != null; 372 return package1.getId().toLowerCase().compareTo(package2.getId().toLowerCase()); 373 }); 374 375 return packages; 376 } 377 isContributedToolUsed(ContributedPlatform platformToIgnore, ContributedTool tool)378 public boolean isContributedToolUsed(ContributedPlatform platformToIgnore, ContributedTool tool) { 379 for (ContributedPackage pack : index.getPackages()) { 380 for (ContributedPlatform p : pack.getPlatforms()) { 381 if (platformToIgnore.equals(p)) { 382 continue; 383 } 384 if (!p.isInstalled() || p.isReadOnly()) { 385 continue; 386 } 387 for (ContributedTool requiredTool : p.getResolvedTools()) { 388 if (requiredTool.equals(tool)) 389 return true; 390 } 391 } 392 } 393 return false; 394 } 395 getInstalledTools()396 public Set<ContributedTool> getInstalledTools() { 397 Set<ContributedTool> tools = new HashSet<>(); 398 if (index == null) { 399 return tools; 400 } 401 for (ContributedPackage pack : index.getPackages()) { 402 Collection<ContributedPlatform> platforms = pack.getPlatforms().stream().filter(new InstalledPredicate()).collect(Collectors.toList()); 403 Map<String, List<ContributedPlatform>> platformsByName = platforms.stream().collect(Collectors.groupingBy(ContributedPlatform::getName)); 404 405 platformsByName.forEach((platformName, platformsWithName) -> { 406 if (platformsWithName.size() > 1) { 407 platformsWithName = platformsWithName.stream().filter(new BuiltInPredicate().negate()).collect(Collectors.toList()); 408 } 409 for (ContributedPlatform p : platformsWithName) { 410 tools.addAll(p.getResolvedTools()); 411 } 412 }); 413 } 414 return tools; 415 } 416 getIndex()417 public ContributionsIndex getIndex() { 418 return index; 419 } 420 getPackagesFolder()421 public File getPackagesFolder() { 422 return packagesFolder; 423 } 424 getStagingFolder()425 public File getStagingFolder() { 426 return stagingFolder; 427 } 428 getIndexFile(String name)429 public File getIndexFile(String name) { 430 return new File(preferencesFolder, name); 431 } 432 getPackages()433 public List<ContributedPackage> getPackages() { 434 if (index == null) { 435 return new LinkedList<>(); 436 } 437 return index.getPackages(); 438 } 439 getCategories()440 public List<String> getCategories() { 441 if (index == null) { 442 return new LinkedList<>(); 443 } 444 return index.getCategories(); 445 } 446 getInstalled(String packageName, String platformArch)447 public ContributedPlatform getInstalled(String packageName, String platformArch) { 448 if (index == null) { 449 return null; 450 } 451 return index.getInstalledPlatform(packageName, platformArch); 452 } 453 getInstalledPlatforms()454 private List<ContributedPlatform> getInstalledPlatforms() { 455 if (index == null) { 456 return new LinkedList<>(); 457 } 458 return index.getInstalledPlatforms(); 459 } 460 isFolderInsidePlatform(final File folder)461 public boolean isFolderInsidePlatform(final File folder) { 462 return getPlatformByFolder(folder) != null; 463 } 464 getPlatformByFolder(final File folder)465 public ContributedPlatform getPlatformByFolder(final File folder) { 466 Optional<ContributedPlatform> platformOptional = getInstalledPlatforms().stream().filter(contributedPlatform -> { 467 assert contributedPlatform.getInstalledFolder() != null; 468 return FileUtils.isSubDirectory(contributedPlatform.getInstalledFolder(), folder); 469 }).findFirst(); 470 471 return platformOptional.orElse(null); 472 } 473 getContributedPlaform(TargetPlatform targetPlatform)474 public ContributedPlatform getContributedPlaform(TargetPlatform targetPlatform) { 475 for (ContributedPlatform plat : getInstalledPlatforms()) { 476 if (plat.getInstalledFolder().equals(targetPlatform.getFolder())) 477 return plat; 478 } 479 return null; 480 } 481 } 482