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