1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.owasp.dependencycheck.analyzer;
19
20 import com.github.packageurl.MalformedPackageURLException;
21 import com.github.packageurl.PackageURL;
22 import com.github.packageurl.PackageURLBuilder;
23 import com.google.common.base.Strings;
24 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
25 import java.io.File;
26 import java.io.FileFilter;
27 import java.io.FileInputStream;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.InputStreamReader;
32 import java.io.Reader;
33 import java.nio.charset.StandardCharsets;
34 import java.nio.file.Paths;
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.Enumeration;
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Map.Entry;
43 import java.util.Properties;
44 import java.util.Set;
45 import java.util.StringTokenizer;
46 import java.util.concurrent.atomic.AtomicInteger;
47 import java.util.jar.Attributes;
48 import java.util.jar.JarEntry;
49 import java.util.jar.JarFile;
50 import java.util.jar.Manifest;
51 import java.util.regex.Pattern;
52 import java.util.zip.ZipEntry;
53
54 import org.apache.commons.io.FilenameUtils;
55 import org.apache.commons.lang3.StringUtils;
56 import org.apache.commons.io.IOUtils;
57 import org.jsoup.Jsoup;
58 import org.owasp.dependencycheck.Engine;
59 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
60 import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
61 import org.owasp.dependencycheck.dependency.Confidence;
62 import org.owasp.dependencycheck.dependency.Dependency;
63 import org.owasp.dependencycheck.dependency.EvidenceType;
64 import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
65 import org.owasp.dependencycheck.dependency.naming.Identifier;
66 import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
67 import org.owasp.dependencycheck.exception.InitializationException;
68 import org.owasp.dependencycheck.utils.FileFilterBuilder;
69 import org.owasp.dependencycheck.utils.FileUtils;
70 import org.owasp.dependencycheck.utils.Settings;
71 import org.owasp.dependencycheck.xml.pom.Developer;
72 import org.owasp.dependencycheck.xml.pom.License;
73 import org.owasp.dependencycheck.xml.pom.Model;
74 import org.owasp.dependencycheck.xml.pom.PomUtils;
75 import org.slf4j.Logger;
76 import org.slf4j.LoggerFactory;
77
78
79
80
81
82
83
84 public class JarAnalyzer extends AbstractFileTypeAnalyzer {
85
86
87
88
89
90
91 public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.JAVA;
92
93
94
95 private static final Logger LOGGER = LoggerFactory.getLogger(JarAnalyzer.class);
96
97
98
99
100 private static final AtomicInteger DIR_COUNT = new AtomicInteger(0);
101
102
103
104 private static final String NEWLINE = System.getProperty("line.separator");
105
106
107
108
109 private static final Set<String> IGNORE_VALUES = newHashSet(
110 "Sun Java System Application Server");
111
112
113
114 private static final Set<String> IGNORE_KEYS = newHashSet(
115 "built-by",
116 "created-by",
117 "builtby",
118 "built-with",
119 "builtwith",
120 "createdby",
121 "build-jdk",
122 "buildjdk",
123 "ant-version",
124 "antversion",
125 "dynamicimportpackage",
126 "dynamicimport-package",
127 "dynamic-importpackage",
128 "dynamic-import-package",
129 "import-package",
130 "ignore-package",
131 "export-package",
132 "importpackage",
133 "import-template",
134 "importtemplate",
135 "java-vendor",
136 "export-template",
137 "exporttemplate",
138 "ignorepackage",
139 "exportpackage",
140 "sealed",
141 "manifest-version",
142 "archiver-version",
143 "manifestversion",
144 "archiverversion",
145 "classpath",
146 "class-path",
147 "tool",
148 "bundle-manifestversion",
149 "bundlemanifestversion",
150 "bundle-vendor",
151 "include-resource",
152 "embed-dependency",
153 "embedded-artifacts",
154 "ipojo-components",
155 "ipojo-extension",
156 "plugin-dependencies",
157 "today",
158 "tstamp",
159 "dstamp",
160 "eclipse-sourcereferences",
161 "kotlin-version",
162 "require-capability",
163 "require-bundle");
164
165
166
167
168 @SuppressWarnings("deprecation")
169 private static final String IMPLEMENTATION_VENDOR_ID = Attributes.Name.IMPLEMENTATION_VENDOR_ID
170 .toString();
171
172
173
174 private static final String BUNDLE_VERSION = "Bundle-Version";
175
176
177
178 private static final String BUNDLE_DESCRIPTION = "Bundle-Description";
179
180
181
182 private static final String BUNDLE_NAME = "Bundle-Name";
183
184
185
186 private static final Pattern HTML_DETECTION_PATTERN = Pattern.compile("\\<[a-z]+.*/?\\>", Pattern.CASE_INSENSITIVE);
187
188
189
190 private static final String ANALYZER_NAME = "Jar Analyzer";
191
192
193
194 private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
195
196
197
198 private static final List<String> EXCLUDE_JARS = Arrays.asList("-doc.jar", "-src.jar", "-javadoc.jar", "-sources.jar");
199
200
201
202 private static final String[] EXTENSIONS = {"jar", "war", "aar"};
203
204
205
206 private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(EXTENSIONS).build();
207
208
209
210
211 private static final byte[] ZIP_FIRST_BYTES = new byte[]{0x50, 0x4B, 0x03, 0x04};
212
213
214
215
216 private static final byte[] ZIP_EMPTY_FIRST_BYTES = new byte[]{0x50, 0x4B, 0x05, 0x06};
217
218
219
220
221 private static final byte[] ZIP_SPANNED_FIRST_BYTES = new byte[]{0x50, 0x4B, 0x07, 0x08};
222
223
224
225
226
227 private File tempFileLocation = null;
228
229
230
231
232
233 private static final String VALID_NAME = "^[A-Za-z0-9_\\-.]+$";
234
235
236
237
238
239
240
241 @Override
242 protected FileFilter getFileFilter() {
243 return FILTER;
244 }
245
246
247
248
249
250
251 @Override
252 public String getName() {
253 return ANALYZER_NAME;
254 }
255
256
257
258
259
260
261 @Override
262 public AnalysisPhase getAnalysisPhase() {
263 return ANALYSIS_PHASE;
264 }
265
266 @Override
267 public boolean accept(File pathname) {
268 final boolean accepted = super.accept(pathname);
269 return accepted && !isExcludedJar(pathname);
270 }
271
272
273
274
275
276
277
278
279
280 private boolean isExcludedJar(File path) {
281 final String fileName = path.getName().toLowerCase();
282 return EXCLUDE_JARS.stream().anyMatch(fileName::endsWith);
283 }
284
285
286
287
288
289
290
291
292 @Override
293 protected String getAnalyzerEnabledSettingKey() {
294 return Settings.KEYS.ANALYZER_JAR_ENABLED;
295 }
296
297
298
299
300
301
302
303
304
305
306 @Override
307 public void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
308 final List<ClassNameInformation> classNames = collectClassNames(dependency);
309 final String fileName = dependency.getFileName().toLowerCase();
310 if ((classNames.isEmpty()
311 && (fileName.endsWith("-sources.jar")
312 || fileName.endsWith("-javadoc.jar")
313 || fileName.endsWith("-src.jar")
314 || fileName.endsWith("-doc.jar")
315 || isMacOSMetaDataFile(dependency, engine)))
316 || !isZipFile(dependency)) {
317 engine.removeDependency(dependency);
318 return;
319 }
320 Exception exception = null;
321 boolean hasManifest = false;
322 try {
323 hasManifest = parseManifest(dependency, classNames);
324 } catch (IOException ex) {
325 LOGGER.debug("Invalid Manifest", ex);
326 exception = ex;
327 }
328 boolean hasPOM = false;
329 try {
330 hasPOM = analyzePOM(dependency, classNames, engine);
331 } catch (AnalysisException ex) {
332 LOGGER.debug("Error parsing pom.xml", ex);
333 exception = ex;
334 }
335 final boolean addPackagesAsEvidence = !(hasManifest && hasPOM);
336 analyzePackageNames(classNames, dependency, addPackagesAsEvidence);
337 dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
338
339 if (exception != null) {
340 throw new AnalysisException(String.format("An error occurred extracting evidence from "
341 + "%s, analysis may be incomplete; please see the log for more details.",
342 dependency.getDisplayFileName()), exception);
343 }
344 }
345
346
347
348
349
350
351
352
353
354
355
356
357 @SuppressFBWarnings(justification = "If actual file path is not null the path will have elements and getFileName will not be called on a null",
358 value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"})
359 private boolean isMacOSMetaDataFile(final Dependency dependency, final Engine engine) {
360 if (dependency.getActualFilePath() != null) {
361 final String fileName = Paths.get(dependency.getActualFilePath()).getFileName().toString();
362 return fileName.startsWith("._") && hasDependencyWithFilename(engine.getDependencies(), fileName.substring(2));
363 }
364 return false;
365 }
366
367
368
369
370
371
372
373
374
375
376
377 @SuppressFBWarnings(justification = "If actual file path is not null the path will have elements and getFileName will not be called on a null",
378 value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"})
379 private boolean hasDependencyWithFilename(final Dependency[] dependencies, final String fileName) {
380 for (final Dependency dependency : dependencies) {
381 if (dependency.getActualFilePath() != null
382 && Paths.get(dependency.getActualFilePath()).getFileName().toString().equalsIgnoreCase(fileName)) {
383 return true;
384 }
385 }
386 return false;
387 }
388
389
390
391
392
393
394
395
396
397
398
399 @SuppressFBWarnings(justification = "try with resources will clean up the output stream", value = {"OBL_UNSATISFIED_OBLIGATION"})
400 private boolean isZipFile(final Dependency dependency) {
401 final byte[] buffer = new byte[4];
402 try (FileInputStream fileInputStream = new FileInputStream(dependency.getActualFilePath())) {
403 if (fileInputStream.read(buffer) > 0
404 && (Arrays.equals(buffer, ZIP_FIRST_BYTES)
405 || Arrays.equals(buffer, ZIP_EMPTY_FIRST_BYTES)
406 || Arrays.equals(buffer, ZIP_SPANNED_FIRST_BYTES))) {
407 return true;
408 }
409 } catch (Exception e) {
410 LOGGER.warn("Unable to check if '{}' is a zip file", dependency.getActualFilePath());
411 LOGGER.trace("", e);
412 }
413 return false;
414 }
415
416
417
418
419
420
421
422
423
424
425
426
427
428 protected boolean analyzePOM(Dependency dependency, List<ClassNameInformation> classes, Engine engine) throws AnalysisException {
429
430
431 boolean evidenceAdded = false;
432 try (JarFile jar = new JarFile(dependency.getActualFilePath(), false)) {
433
434 final String repoPomName = FilenameUtils.removeExtension(dependency.getActualFilePath()) + ".pom";
435 final File repoPom = new File(repoPomName);
436 if (repoPom.isFile()) {
437 final Model pom = PomUtils.readPom(repoPom);
438 evidenceAdded |= setPomEvidence(dependency, pom, classes, true);
439 }
440
441 final List<String> pomEntries = retrievePomListing(jar);
442
443 for (String path : pomEntries) {
444 LOGGER.debug("Reading pom entry: {}", path);
445 try {
446
447 final Properties pomProperties = retrievePomProperties(path, jar);
448 final File pomFile = extractPom(path, jar);
449 final Model pom = PomUtils.readPom(pomFile);
450 pom.setGAVFromPomDotProperties(pomProperties);
451 pom.processProperties(pomProperties);
452
453 final String artifactId = new File(path).getParentFile().getName();
454 if (dependency.getActualFile().getName().startsWith(artifactId)) {
455 evidenceAdded |= setPomEvidence(dependency, pom, classes, true);
456 } else {
457 final String displayPath = String.format("%s%s%s",
458 dependency.getFilePath(),
459 File.separator,
460 path);
461 final String displayName = String.format("%s%s%s",
462 dependency.getFileName(),
463 File.separator,
464 path);
465 final Dependency newDependency = new Dependency();
466 newDependency.setActualFilePath(pomFile.getAbsolutePath());
467 newDependency.setFileName(displayName);
468 newDependency.setFilePath(displayPath);
469 newDependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
470 String groupId = pom.getGroupId();
471 String version = pom.getVersion();
472 if (groupId == null) {
473 groupId = pom.getParentGroupId();
474 }
475 if (version == null) {
476 version = pom.getParentVersion();
477 }
478 if (groupId == null) {
479 newDependency.setName(pom.getArtifactId());
480 newDependency.setPackagePath(String.format("%s:%s", pom.getArtifactId(), version));
481 } else {
482 newDependency.setName(String.format("%s:%s", groupId, pom.getArtifactId()));
483 newDependency.setPackagePath(String.format("%s:%s:%s", groupId, pom.getArtifactId(), version));
484 }
485 newDependency.setDisplayFileName(String.format("%s (shaded: %s)",
486 dependency.getDisplayFileName(), newDependency.getPackagePath()));
487 newDependency.setVersion(version);
488 setPomEvidence(newDependency, pom, null, true);
489 if (dependency.getProjectReferences().size() > 0) {
490 newDependency.addAllProjectReferences(dependency.getProjectReferences());
491 }
492 engine.addDependency(newDependency);
493 }
494 } catch (AnalysisException ex) {
495 LOGGER.warn("An error occurred while analyzing '{}'.", dependency.getActualFilePath());
496 LOGGER.trace("", ex);
497 }
498 }
499 } catch (IOException ex) {
500 LOGGER.warn("Unable to read JarFile '{}'.", dependency.getActualFilePath());
501 LOGGER.trace("", ex);
502 }
503 return evidenceAdded;
504 }
505
506
507
508
509
510
511
512
513
514 private Properties retrievePomProperties(String path, final JarFile jar) {
515 final Properties pomProperties = new Properties();
516 final String propPath = path.substring(0, path.length() - 7) + "pom.properties";
517 final ZipEntry propEntry = jar.getEntry(propPath);
518 if (propEntry != null) {
519 try (Reader reader = new InputStreamReader(jar.getInputStream(propEntry), StandardCharsets.UTF_8)) {
520 pomProperties.load(reader);
521 LOGGER.debug("Read pom.properties: {}", propPath);
522 } catch (IOException ex) {
523 LOGGER.trace("Unable to read the POM properties", ex);
524 }
525 }
526 return pomProperties;
527 }
528
529
530
531
532
533
534
535
536
537 private List<String> retrievePomListing(final JarFile jar) throws IOException {
538 final List<String> pomEntries = new ArrayList<>();
539 final Enumeration<JarEntry> entries = jar.entries();
540 while (entries.hasMoreElements()) {
541 final JarEntry entry = entries.nextElement();
542 final String entryName = new File(entry.getName()).getName().toLowerCase();
543 if (!entry.isDirectory() && "pom.xml".equals(entryName)
544 && entry.getName().toUpperCase().startsWith("META-INF")) {
545 pomEntries.add(entry.getName());
546 }
547 }
548 return pomEntries;
549 }
550
551
552
553
554
555
556
557
558
559
560 private File extractPom(String path, JarFile jar) throws AnalysisException {
561 final File tmpDir = getNextTempDirectory();
562 final File file = new File(tmpDir, "pom.xml");
563 final ZipEntry entry = jar.getEntry(path);
564 if (entry == null) {
565 throw new AnalysisException(String.format("Pom (%s) does not exist in %s", path, jar.getName()));
566 }
567 try (InputStream input = jar.getInputStream(entry);
568 FileOutputStream fos = new FileOutputStream(file)) {
569 IOUtils.copy(input, fos);
570 } catch (IOException ex) {
571 LOGGER.warn("An error occurred reading '{}' from '{}'.", path, jar.getName());
572 LOGGER.error("", ex);
573 }
574 return file;
575 }
576
577
578
579
580
581
582
583
584
585
586
587
588 public static boolean setPomEvidence(Dependency dependency, Model pom,
589 List<ClassNameInformation> classes, boolean isMainPom) {
590 if (pom == null) {
591 return false;
592 }
593 boolean foundSomething = false;
594 boolean addAsIdentifier = true;
595 String groupid = intepolationFailCheck(pom.getGroupId());
596 String parentGroupId = intepolationFailCheck(pom.getParentGroupId());
597 String artifactid = intepolationFailCheck(pom.getArtifactId());
598 String parentArtifactId = intepolationFailCheck(pom.getParentArtifactId());
599 String version = intepolationFailCheck(pom.getVersion());
600 String parentVersion = intepolationFailCheck(pom.getParentVersion());
601
602 if (("org.sonatype.oss".equals(parentGroupId) && "oss-parent".equals(parentArtifactId))
603 || ("org.springframework.boot".equals(parentGroupId) && "spring-boot-starter-parent".equals(parentArtifactId))) {
604 parentGroupId = null;
605 parentArtifactId = null;
606 parentVersion = null;
607 }
608
609 if ((groupid == null || groupid.isEmpty()) && parentGroupId != null && !parentGroupId.isEmpty()) {
610 groupid = parentGroupId;
611 }
612
613 final String originalGroupID = groupid;
614
615 if ((artifactid == null || artifactid.isEmpty()) && parentArtifactId != null && !parentArtifactId.isEmpty()) {
616 artifactid = parentArtifactId;
617 }
618
619 final String originalArtifactID = artifactid;
620 if (artifactid != null && (artifactid.startsWith("org.") || artifactid.startsWith("com."))) {
621 artifactid = artifactid.substring(4);
622 }
623
624 if ((version == null || version.isEmpty()) && parentVersion != null && !parentVersion.isEmpty()) {
625 version = parentVersion;
626 }
627
628 if (isMainPom && dependency.getName() == null && originalArtifactID != null && !originalArtifactID.isEmpty()) {
629 if (originalGroupID != null && !originalGroupID.isEmpty()) {
630 dependency.setName(String.format("%s:%s", originalGroupID, originalArtifactID));
631 } else {
632 dependency.setName(originalArtifactID);
633 }
634 }
635 if (isMainPom && dependency.getVersion() == null && version != null && !version.isEmpty()) {
636 dependency.setVersion(version);
637 }
638
639 if (groupid != null && !groupid.isEmpty()) {
640 foundSomething = true;
641 dependency.addEvidence(EvidenceType.VENDOR, "pom", "groupid", groupid, Confidence.HIGHEST);
642
643
644
645 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "groupid", groupid, Confidence.HIGHEST);
646 addMatchingValues(classes, groupid, dependency, EvidenceType.VENDOR);
647 addMatchingValues(classes, groupid, dependency, EvidenceType.PRODUCT);
648 if (parentGroupId != null && !parentGroupId.isEmpty() && !parentGroupId.equals(groupid)) {
649 dependency.addEvidence(EvidenceType.VENDOR, "pom", "parent-groupid", parentGroupId, Confidence.MEDIUM);
650
651
652 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "parent-groupid", parentGroupId, Confidence.MEDIUM);
653 addMatchingValues(classes, parentGroupId, dependency, EvidenceType.VENDOR);
654 addMatchingValues(classes, parentGroupId, dependency, EvidenceType.PRODUCT);
655 }
656 } else {
657 addAsIdentifier = false;
658 }
659
660 if (artifactid != null && !artifactid.isEmpty()) {
661 foundSomething = true;
662 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "artifactid", artifactid, Confidence.HIGHEST);
663 dependency.addEvidence(EvidenceType.VENDOR, "pom", "artifactid", artifactid, Confidence.LOW);
664 addMatchingValues(classes, artifactid, dependency, EvidenceType.VENDOR);
665 addMatchingValues(classes, artifactid, dependency, EvidenceType.PRODUCT);
666 if (parentArtifactId != null && !parentArtifactId.isEmpty() && !parentArtifactId.equals(artifactid)) {
667 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "parent-artifactid", parentArtifactId, Confidence.MEDIUM);
668 dependency.addEvidence(EvidenceType.VENDOR, "pom", "parent-artifactid", parentArtifactId, Confidence.LOW);
669 addMatchingValues(classes, parentArtifactId, dependency, EvidenceType.VENDOR);
670 addMatchingValues(classes, parentArtifactId, dependency, EvidenceType.PRODUCT);
671 }
672 } else {
673 addAsIdentifier = false;
674 }
675
676 if (version != null && !version.isEmpty()) {
677 foundSomething = true;
678 dependency.addEvidence(EvidenceType.VERSION, "pom", "version", version, Confidence.HIGHEST);
679 if (parentVersion != null && !parentVersion.isEmpty() && !parentVersion.equals(version)) {
680 dependency.addEvidence(EvidenceType.VERSION, "pom", "parent-version", version, Confidence.LOW);
681 }
682 } else {
683 addAsIdentifier = false;
684 }
685
686 if (addAsIdentifier && isMainPom) {
687 Identifier id = null;
688 try {
689 if (originalArtifactID != null && originalArtifactID.matches(VALID_NAME)
690 && originalGroupID != null && originalGroupID.matches(VALID_NAME)) {
691 final PackageURL purl = PackageURLBuilder.aPackageURL().withType("maven").withNamespace(originalGroupID)
692 .withName(originalArtifactID).withVersion(version).build();
693 id = new PurlIdentifier(purl, Confidence.HIGH);
694 } else {
695 LOGGER.debug("Invalid maven identifier identified: " + originalGroupID + ":" + originalArtifactID);
696 }
697 } catch (MalformedPackageURLException ex) {
698 final String gav = String.format("%s:%s:%s", originalGroupID, originalArtifactID, version);
699 LOGGER.debug("Error building package url for " + gav + "; using generic identifier instead.", ex);
700 id = new GenericIdentifier("maven:" + gav, Confidence.HIGH);
701 }
702 if (id != null) {
703 dependency.addSoftwareIdentifier(id);
704 }
705 }
706
707
708 final String org = pom.getOrganization();
709 if (org != null && !org.isEmpty()) {
710 dependency.addEvidence(EvidenceType.VENDOR, "pom", "organization name", org, Confidence.HIGH);
711 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "organization name", org, Confidence.LOW);
712 addMatchingValues(classes, org, dependency, EvidenceType.VENDOR);
713 addMatchingValues(classes, org, dependency, EvidenceType.PRODUCT);
714 }
715
716 String orgUrl = pom.getOrganizationUrl();
717 if (orgUrl != null && !orgUrl.isEmpty()) {
718 if (orgUrl.startsWith("https://github.com/") || orgUrl.startsWith("https://gitlab.com/")) {
719 orgUrl = orgUrl.substring(19);
720 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "url", orgUrl, Confidence.HIGH);
721 } else {
722 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "organization url", orgUrl, Confidence.LOW);
723 }
724 dependency.addEvidence(EvidenceType.VENDOR, "pom", "organization url", orgUrl, Confidence.MEDIUM);
725 }
726
727 final String pomName = pom.getName();
728 if (pomName != null && !pomName.isEmpty() && !"${project.groupId}:${project.artifactId}".equals(pomName)) {
729 foundSomething = true;
730 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "name", pomName, Confidence.HIGH);
731 dependency.addEvidence(EvidenceType.VENDOR, "pom", "name", pomName, Confidence.HIGH);
732 addMatchingValues(classes, pomName, dependency, EvidenceType.VENDOR);
733 addMatchingValues(classes, pomName, dependency, EvidenceType.PRODUCT);
734 }
735
736
737 final String description = pom.getDescription();
738 if (description != null && !description.isEmpty()
739 && !description.startsWith("POM was created by")
740 && !description.startsWith("Sonatype helps open source projects")
741 && !description.endsWith("project for Spring Boot")) {
742 foundSomething = true;
743 final String trimmedDescription = addDescription(dependency, description, "pom", "description");
744 addMatchingValues(classes, trimmedDescription, dependency, EvidenceType.VENDOR);
745 addMatchingValues(classes, trimmedDescription, dependency, EvidenceType.PRODUCT);
746 }
747
748 String projectURL = pom.getProjectURL();
749 if (projectURL != null && !projectURL.trim().isEmpty()) {
750 if (projectURL.startsWith("https://github.com/") || projectURL.startsWith("https://gitlab.com/")) {
751 projectURL = projectURL.substring(19);
752 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "url", projectURL, Confidence.HIGH);
753 } else {
754 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "url", projectURL, Confidence.MEDIUM);
755 }
756 dependency.addEvidence(EvidenceType.VENDOR, "pom", "url", projectURL, Confidence.HIGHEST);
757
758 }
759
760 if (pom.getDevelopers() != null && !pom.getDevelopers().isEmpty()) {
761 for (Developer dev : pom.getDevelopers()) {
762 if (!Strings.isNullOrEmpty(dev.getId())) {
763 dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer id", dev.getId(), Confidence.MEDIUM);
764 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer id", dev.getId(), Confidence.LOW);
765 }
766 if (!Strings.isNullOrEmpty(dev.getName())) {
767 dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer name", dev.getName(), Confidence.MEDIUM);
768 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer name", dev.getName(), Confidence.LOW);
769 }
770 if (!Strings.isNullOrEmpty(dev.getEmail())) {
771 dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer email", dev.getEmail(), Confidence.LOW);
772 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer email", dev.getEmail(), Confidence.LOW);
773 }
774 if (!Strings.isNullOrEmpty(dev.getOrganizationUrl())) {
775 dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer org URL", dev.getOrganizationUrl(), Confidence.MEDIUM);
776 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer org URL", dev.getOrganizationUrl(), Confidence.LOW);
777 }
778 final String devOrg = dev.getOrganization();
779 if (!Strings.isNullOrEmpty(devOrg)) {
780 dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer org", devOrg, Confidence.MEDIUM);
781 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer org", devOrg, Confidence.LOW);
782 addMatchingValues(classes, devOrg, dependency, EvidenceType.VENDOR);
783 addMatchingValues(classes, devOrg, dependency, EvidenceType.PRODUCT);
784 }
785 }
786 }
787
788 extractLicense(pom, dependency);
789 return foundSomething;
790 }
791
792
793
794
795
796
797
798
799
800
801
802
803 protected void analyzePackageNames(List<ClassNameInformation> classNames,
804 Dependency dependency, boolean addPackagesAsEvidence) {
805 final Map<String, Integer> vendorIdentifiers = new HashMap<>();
806 final Map<String, Integer> productIdentifiers = new HashMap<>();
807 analyzeFullyQualifiedClassNames(classNames, vendorIdentifiers, productIdentifiers);
808
809 final int classCount = classNames.size();
810
811 vendorIdentifiers.forEach((key, value) -> {
812 final float ratio = value / (float) classCount;
813 if (ratio > 0.5) {
814
815 dependency.addVendorWeighting(key);
816 if (addPackagesAsEvidence && key.length() > 1) {
817 dependency.addEvidence(EvidenceType.VENDOR, "jar", "package name", key, Confidence.LOW);
818 }
819 }
820 });
821 productIdentifiers.forEach((key, value) -> {
822 final float ratio = value / (float) classCount;
823 if (ratio > 0.5) {
824
825 dependency.addProductWeighting(key);
826 if (addPackagesAsEvidence && key.length() > 1) {
827 dependency.addEvidence(EvidenceType.PRODUCT, "jar", "package name", key, Confidence.LOW);
828 }
829 }
830 });
831 }
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850 protected boolean parseManifest(Dependency dependency, List<ClassNameInformation> classInformation)
851 throws IOException {
852 boolean foundSomething = false;
853 try (JarFile jar = new JarFile(dependency.getActualFilePath(), false)) {
854 final Manifest manifest = jar.getManifest();
855 if (manifest == null) {
856 if (!dependency.getFileName().toLowerCase().endsWith("-sources.jar")
857 && !dependency.getFileName().toLowerCase().endsWith("-javadoc.jar")
858 && !dependency.getFileName().toLowerCase().endsWith("-src.jar")
859 && !dependency.getFileName().toLowerCase().endsWith("-doc.jar")) {
860 LOGGER.debug("Jar file '{}' does not contain a manifest.", dependency.getFileName());
861 }
862 return false;
863 }
864 String source = "Manifest";
865 String specificationVersion = null;
866 boolean hasImplementationVersion = false;
867 Attributes atts = manifest.getMainAttributes();
868 for (Entry<Object, Object> entry : atts.entrySet()) {
869 String key = entry.getKey().toString();
870 String value = atts.getValue(key);
871 if (HTML_DETECTION_PATTERN.matcher(value).find()) {
872 value = Jsoup.parse(value).text();
873 }
874 if (value.startsWith("git@github.com:") || value.startsWith("git@gitlab.com:")) {
875 value = value.substring(15);
876 }
877 if (IGNORE_VALUES.contains(value)) {
878 continue;
879 } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_TITLE.toString())) {
880 foundSomething = true;
881 dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.HIGH);
882 addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
883 } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VERSION.toString())) {
884 hasImplementationVersion = true;
885 foundSomething = true;
886 dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.HIGH);
887 } else if ("specification-version".equalsIgnoreCase(key)) {
888 specificationVersion = value;
889 } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VENDOR.toString())) {
890 foundSomething = true;
891 dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.HIGH);
892 addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
893 } else if (key.equalsIgnoreCase(IMPLEMENTATION_VENDOR_ID)) {
894 foundSomething = true;
895 dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM);
896 addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
897 } else if (key.equalsIgnoreCase(BUNDLE_DESCRIPTION)) {
898 if (!value.startsWith("Sonatype helps open source projects")) {
899 foundSomething = true;
900 addDescription(dependency, value, "manifest", key);
901 addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
902 }
903 } else if (key.equalsIgnoreCase(BUNDLE_NAME)) {
904 foundSomething = true;
905 dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
906 addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
907
908
909 } else if (key.equalsIgnoreCase(BUNDLE_VERSION)) {
910 foundSomething = true;
911 dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.HIGH);
912 } else if (key.equalsIgnoreCase(Attributes.Name.MAIN_CLASS.toString())) {
913
914 continue;
915
916 } else if ("implementation-url".equalsIgnoreCase(key)
917 && value != null
918 && value.startsWith("https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/parent/")) {
919 continue;
920 } else {
921 key = key.toLowerCase();
922 if (!IGNORE_KEYS.contains(key)
923 && !key.endsWith("jdk")
924 && !key.contains("lastmodified")
925 && !key.endsWith("package")
926 && !key.endsWith("classpath")
927 && !key.endsWith("class-path")
928 && !key.endsWith("-scm")
929 && !key.startsWith("scm-")
930 && !value.trim().startsWith("scm:")
931 && !isImportPackage(key, value)
932 && !isPackage(key, value)) {
933 foundSomething = true;
934 if (key.contains("version")) {
935 if (!key.contains("specification")) {
936 dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.MEDIUM);
937 }
938 } else if ("build-id".equals(key)) {
939 int pos = value.indexOf('(');
940 if (pos > 0) {
941 value = value.substring(0, pos - 1);
942 }
943 pos = value.indexOf('[');
944 if (pos > 0) {
945 value = value.substring(0, pos - 1);
946 }
947 dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.MEDIUM);
948 } else if (key.contains("title")) {
949 dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
950 addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
951 } else if (key.contains("vendor")) {
952 if (key.contains("specification")) {
953 dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.LOW);
954 } else {
955 dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM);
956 addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
957 }
958 } else if (key.contains("name")) {
959 dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
960 dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM);
961 addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
962 addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
963 } else if (key.contains("license")) {
964 addLicense(dependency, value);
965 } else if (key.contains("description")) {
966 if (!value.startsWith("Sonatype helps open source projects")) {
967 final String trimmedDescription = addDescription(dependency, value, "manifest", key);
968 addMatchingValues(classInformation, trimmedDescription, dependency, EvidenceType.VENDOR);
969 addMatchingValues(classInformation, trimmedDescription, dependency, EvidenceType.PRODUCT);
970 }
971 } else {
972 dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.LOW);
973 dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.LOW);
974 addMatchingValues(classInformation, value, dependency, EvidenceType.VERSION);
975 addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
976 if (value.matches(".*\\d.*")) {
977 final StringTokenizer tokenizer = new StringTokenizer(value, " ");
978 while (tokenizer.hasMoreElements()) {
979 final String s = tokenizer.nextToken();
980 if (s.matches("^[0-9.]+$")) {
981 dependency.addEvidence(EvidenceType.VERSION, source, key, s, Confidence.LOW);
982 }
983 }
984 }
985 }
986 }
987 }
988 }
989 for (Map.Entry<String, Attributes> item : manifest.getEntries().entrySet()) {
990 final String name = item.getKey();
991 source = "manifest: " + name;
992 atts = item.getValue();
993 for (Entry<Object, Object> entry : atts.entrySet()) {
994 final String key = entry.getKey().toString();
995 final String value = atts.getValue(key);
996 if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_TITLE.toString())) {
997 foundSomething = true;
998 dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
999 addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
1000 } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VERSION.toString())) {
1001 foundSomething = true;
1002 dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.MEDIUM);
1003 } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VENDOR.toString())) {
1004 foundSomething = true;
1005 dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM);
1006 addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
1007 } else if (key.equalsIgnoreCase(Attributes.Name.SPECIFICATION_TITLE.toString())) {
1008 foundSomething = true;
1009 dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
1010 addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
1011 }
1012 }
1013 }
1014 if (specificationVersion != null && !hasImplementationVersion) {
1015 foundSomething = true;
1016 dependency.addEvidence(EvidenceType.VERSION, source, "specification-version", specificationVersion, Confidence.HIGH);
1017 }
1018 }
1019 return foundSomething;
1020 }
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037 public static String addDescription(Dependency dependency, String description, String source, String key) {
1038 if (dependency.getDescription() == null) {
1039 dependency.setDescription(description);
1040 }
1041 String desc;
1042 if (HTML_DETECTION_PATTERN.matcher(description).find()) {
1043 desc = Jsoup.parse(description).text();
1044 } else {
1045 desc = description;
1046 }
1047 dependency.setDescription(desc);
1048 if (desc.length() > 100) {
1049 desc = desc.replaceAll("\\s\\s+", " ");
1050 final int posSuchAs = desc.toLowerCase().indexOf("such as ", 100);
1051 final int posLike = desc.toLowerCase().indexOf("like ", 100);
1052 final int posWillUse = desc.toLowerCase().indexOf("will use ", 100);
1053 final int posUses = desc.toLowerCase().indexOf(" uses ", 100);
1054
1055 int pos = -1;
1056 pos = Math.max(pos, posSuchAs);
1057 if (pos >= 0 && posLike >= 0) {
1058 pos = Math.min(pos, posLike);
1059 } else {
1060 pos = Math.max(pos, posLike);
1061 }
1062 if (pos >= 0 && posWillUse >= 0) {
1063 pos = Math.min(pos, posWillUse);
1064 } else {
1065 pos = Math.max(pos, posWillUse);
1066 }
1067 if (pos >= 0 && posUses >= 0) {
1068 pos = Math.min(pos, posUses);
1069 } else {
1070 pos = Math.max(pos, posUses);
1071 }
1072 if (pos > 0) {
1073 desc = desc.substring(0, pos) + "...";
1074 }
1075
1076
1077
1078
1079
1080
1081
1082 }
1083 return desc;
1084 }
1085
1086
1087
1088
1089
1090
1091
1092 private void addLicense(Dependency d, String license) {
1093 if (d.getLicense() == null) {
1094 d.setLicense(license);
1095 } else if (!d.getLicense().contains(license)) {
1096 d.setLicense(d.getLicense() + NEWLINE + license);
1097 }
1098 }
1099
1100
1101
1102
1103
1104
1105
1106
1107 @Override
1108 public void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
1109 try {
1110 final File baseDir = getSettings().getTempDirectory();
1111 tempFileLocation = File.createTempFile("check", "tmp", baseDir);
1112 if (!tempFileLocation.delete()) {
1113 final String msg = String.format("Unable to delete temporary file '%s'.", tempFileLocation.getAbsolutePath());
1114 setEnabled(false);
1115 throw new InitializationException(msg);
1116 }
1117 if (!tempFileLocation.mkdirs()) {
1118 final String msg = String.format("Unable to create directory '%s'.", tempFileLocation.getAbsolutePath());
1119 setEnabled(false);
1120 throw new InitializationException(msg);
1121 }
1122 } catch (IOException ex) {
1123 setEnabled(false);
1124 throw new InitializationException("Unable to create a temporary file", ex);
1125 }
1126 }
1127
1128
1129
1130
1131 @Override
1132 public void closeAnalyzer() {
1133 if (tempFileLocation != null && tempFileLocation.exists()) {
1134 LOGGER.debug("Attempting to delete temporary files from `{}`", tempFileLocation.toString());
1135 final boolean success = FileUtils.delete(tempFileLocation);
1136 if (!success && tempFileLocation.exists()) {
1137 final String[] l = tempFileLocation.list();
1138 if (l != null && l.length > 0) {
1139 LOGGER.warn("Failed to delete the JAR Analyzder's temporary files from `{}`, "
1140 + "see the log for more details", tempFileLocation.getAbsolutePath());
1141 }
1142 }
1143 }
1144 }
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155 private boolean isImportPackage(String key, String value) {
1156 final Pattern packageRx = Pattern.compile("^(\\s*[a-zA-Z0-9_#\\$\\*\\.]+\\s*[,;])+(\\s*[a-zA-Z0-9_#\\$\\*\\.]+\\s*)?$");
1157 final boolean matches = packageRx.matcher(value).matches();
1158 return matches && (key.contains("import") || key.contains("include") || value.length() > 10);
1159 }
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169 protected List<ClassNameInformation> collectClassNames(Dependency dependency) {
1170 final List<ClassNameInformation> classNames = new ArrayList<>();
1171 try (JarFile jar = new JarFile(dependency.getActualFilePath(), false)) {
1172 final Enumeration<JarEntry> entries = jar.entries();
1173 while (entries.hasMoreElements()) {
1174 final JarEntry entry = entries.nextElement();
1175 final String name = entry.getName().toLowerCase();
1176
1177 if (name.endsWith(".class") && !name.matches("^javax?\\..*$")) {
1178 final ClassNameInformation className = new ClassNameInformation(name.substring(0, name.length() - 6));
1179 classNames.add(className);
1180 }
1181 }
1182 } catch (IOException ex) {
1183 LOGGER.warn("Unable to open jar file '{}'.", dependency.getFileName());
1184 LOGGER.debug("", ex);
1185 }
1186 return classNames;
1187 }
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201 private void analyzeFullyQualifiedClassNames(List<ClassNameInformation> classNames,
1202 Map<String, Integer> vendor, Map<String, Integer> product) {
1203 for (ClassNameInformation entry : classNames) {
1204 final List<String> list = entry.getPackageStructure();
1205 addEntry(vendor, list.get(0));
1206
1207 if (list.size() == 2) {
1208 addEntry(product, list.get(1));
1209 } else if (list.size() == 3) {
1210 addEntry(vendor, list.get(1));
1211 addEntry(product, list.get(1));
1212 addEntry(product, list.get(2));
1213 } else if (list.size() >= 4) {
1214 addEntry(vendor, list.get(1));
1215 addEntry(vendor, list.get(2));
1216 addEntry(product, list.get(1));
1217 addEntry(product, list.get(2));
1218 addEntry(product, list.get(3));
1219 }
1220 }
1221 }
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231 private void addEntry(Map<String, Integer> collection, String key) {
1232 if (collection.containsKey(key)) {
1233 collection.put(key, collection.get(key) + 1);
1234 } else {
1235 collection.put(key, 1);
1236 }
1237 }
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250 protected static void addMatchingValues(List<ClassNameInformation> classes, String value, Dependency dep, EvidenceType type) {
1251 if (value == null || value.isEmpty() || classes == null || classes.isEmpty()) {
1252 return;
1253 }
1254 final HashSet<String> tested = new HashSet<>();
1255
1256 for (ClassNameInformation cni : classes) {
1257
1258 for (String key : cni.getPackageStructure()) {
1259
1260 if (!tested.contains(key)) {
1261 tested.add(key);
1262 final int pos = StringUtils.indexOfIgnoreCase(value, key);
1263 if ((pos == 0 && (key.length() == value.length() || (key.length() < value.length()
1264 && !Character.isLetterOrDigit(value.charAt(key.length())))))
1265 || (pos > 0 && !Character.isLetterOrDigit(value.charAt(pos - 1))
1266 && (pos + key.length() == value.length() || (key.length() < value.length()
1267 && !Character.isLetterOrDigit(value.charAt(pos + key.length())))))) {
1268 dep.addEvidence(type, "jar", "package name", key, Confidence.HIGHEST);
1269 }
1270 }
1271 }
1272 }
1273 }
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283 private boolean isPackage(String key, String value) {
1284
1285 return !key.matches(".*(version|title|vendor|name|license|description).*")
1286 && value.matches("^[a-zA-Z_][a-zA-Z0-9_\\$]*\\.([a-zA-Z_][a-zA-Z0-9_\\$]*\\.)*([a-zA-Z_][a-zA-Z0-9_\\$]*)$");
1287
1288 }
1289
1290
1291
1292
1293
1294
1295
1296 private static String intepolationFailCheck(String value) {
1297 if (value != null && value.contains("${")) {
1298 return null;
1299 }
1300 return value;
1301 }
1302
1303
1304
1305
1306
1307
1308
1309
1310 public static void extractLicense(Model pom, Dependency dependency) {
1311
1312 if (pom.getLicenses() != null) {
1313 StringBuilder license = null;
1314 for (License lic : pom.getLicenses()) {
1315 String tmp = null;
1316 if (lic.getName() != null) {
1317 tmp = lic.getName();
1318 }
1319 if (lic.getUrl() != null) {
1320 if (tmp == null) {
1321 tmp = lic.getUrl();
1322 } else {
1323 tmp += ": " + lic.getUrl();
1324 }
1325 }
1326 if (tmp == null) {
1327 continue;
1328 }
1329 if (HTML_DETECTION_PATTERN.matcher(tmp).find()) {
1330 tmp = Jsoup.parse(tmp).text();
1331 }
1332 if (license == null) {
1333 license = new StringBuilder(tmp);
1334 } else {
1335 license.append("\n").append(tmp);
1336 }
1337 }
1338 if (license != null) {
1339 dependency.setLicense(license.toString());
1340
1341 }
1342 }
1343 }
1344
1345
1346
1347
1348 protected static class ClassNameInformation {
1349
1350
1351
1352
1353 private String name;
1354
1355
1356
1357
1358 private final ArrayList<String> packageStructure = new ArrayList<>();
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382 ClassNameInformation(String className) {
1383 name = className;
1384 if (name.contains("/")) {
1385 final String[] tmp = StringUtils.split(className.toLowerCase(), '/');
1386 int start = 0;
1387 int end = 3;
1388 if ("com".equals(tmp[0]) || "org".equals(tmp[0])) {
1389 start = 1;
1390 end = 4;
1391 }
1392 if (tmp.length <= end) {
1393 end = tmp.length - 1;
1394 }
1395 packageStructure.addAll(Arrays.asList(tmp).subList(start, end + 1));
1396 } else {
1397 packageStructure.add(name);
1398 }
1399 }
1400
1401
1402
1403
1404
1405
1406 public String getName() {
1407 return name;
1408 }
1409
1410
1411
1412
1413
1414
1415 public void setName(String name) {
1416 this.name = name;
1417 }
1418
1419
1420
1421
1422
1423
1424 public ArrayList<String> getPackageStructure() {
1425 return packageStructure;
1426 }
1427 }
1428
1429
1430
1431
1432
1433
1434
1435 private File getNextTempDirectory() throws AnalysisException {
1436 final int dirCount = DIR_COUNT.incrementAndGet();
1437 final File directory = new File(tempFileLocation, String.valueOf(dirCount));
1438
1439 if (directory.exists()) {
1440 return getNextTempDirectory();
1441 }
1442 if (!directory.mkdirs()) {
1443 final String msg = String.format("Unable to create temp directory '%s'.", directory.getAbsolutePath());
1444 throw new AnalysisException(msg);
1445 }
1446 return directory;
1447 }
1448 }