• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 7302624989

22 Dec 2023 06:01PM UTC coverage: 48.881% (+1.0%) from 47.873%
7302624989

Pull #119

github

web-flow
Merge fe9109f34 into 1d60d9c17
Pull Request #119: #103: security warning for CVEs in file tool/edition/security

1225 of 2649 branches covered (0.0%)

Branch coverage included in aggregate %.

2968 of 5929 relevant lines covered (50.06%)

2.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

48.06
cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java
1
package com.devonfw.tools.ide.tool;
2

3
import java.io.IOException;
4
import java.nio.file.Files;
5
import java.nio.file.Path;
6
import java.nio.file.Paths;
7
import java.util.List;
8
import java.util.Set;
9
import java.util.stream.Collectors;
10
import java.util.stream.Stream;
11

12
import com.devonfw.tools.ide.url.model.file.json.UrlSecurityWarning;
13
import com.devonfw.tools.ide.cli.CliException;
14
import com.devonfw.tools.ide.commandlet.Commandlet;
15
import com.devonfw.tools.ide.common.Tags;
16
import com.devonfw.tools.ide.context.IdeContext;
17
import com.devonfw.tools.ide.environment.EnvironmentVariables;
18
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
19
import com.devonfw.tools.ide.io.FileAccess;
20
import com.devonfw.tools.ide.io.TarCompression;
21
import com.devonfw.tools.ide.os.MacOsHelper;
22
import com.devonfw.tools.ide.process.ProcessContext;
23
import com.devonfw.tools.ide.process.ProcessErrorHandling;
24
import com.devonfw.tools.ide.property.StringListProperty;
25
import com.devonfw.tools.ide.url.model.file.UrlSecurityJsonFile;
26
import com.devonfw.tools.ide.util.FilenameUtil;
27
import com.devonfw.tools.ide.version.VersionIdentifier;
28

29
/**
30
 * {@link Commandlet} for a tool integrated into the IDE.
31
 */
32
public abstract class ToolCommandlet extends Commandlet implements Tags {
1✔
33

34
  /** @see #getName() */
35
  protected final String tool;
36

37
  private final Set<String> tags;
38

39
  /** The commandline arguments to pass to the tool. */
40
  public final StringListProperty arguments;
41

42
  private MacOsHelper macOsHelper;
43

44
  /**
45
   * The constructor.
46
   *
47
   * @param context the {@link IdeContext}.
48
   * @param tool the {@link #getName() tool name}.
49
   * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of}
50
   *        method.
51
   */
52
  public ToolCommandlet(IdeContext context, String tool, Set<String> tags) {
53

54
    super(context);
3✔
55
    this.tool = tool;
3✔
56
    this.tags = tags;
3✔
57
    addKeyword(tool);
3✔
58
    this.arguments = add(new StringListProperty("", false, "args"));
11✔
59
  }
1✔
60

61
  /**
62
   * @return the name of the tool (e.g. "java", "mvn", "npm", "node").
63
   */
64
  @Override
65
  public String getName() {
66

67
    return this.tool;
3✔
68
  }
69

70
  /**
71
   * @return the name of the binary executable for this tool.
72
   */
73
  protected String getBinaryName() {
74

75
    return this.tool;
×
76
  }
77

78
  @Override
79
  public final Set<String> getTags() {
80

81
    return this.tags;
×
82
  }
83

84
  @Override
85
  public void run() {
86

87
    runTool(null, this.arguments.asArray());
×
88
  }
×
89

90
  /**
91
   * Ensures the tool is installed and then runs this tool with the given arguments.
92
   *
93
   * @param toolVersion the explicit version (pattern) to run. Typically {@code null} to ensure the configured version
94
   *        is installed and use that one. Otherwise, the specified version will be installed in the software repository
95
   *        without touching and IDE installation and used to run.
96
   * @param args the commandline arguments to run the tool.
97
   */
98
  public void runTool(VersionIdentifier toolVersion, String... args) {
99

100
    Path binaryPath;
101
    Path toolPath = Paths.get(getBinaryName());
×
102
    if (toolVersion == null) {
×
103
      install(true);
×
104
      binaryPath = toolPath;
×
105
    } else {
106
      throw new UnsupportedOperationException("Not yet implemented!");
×
107
    }
108
    ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.WARNING).executable(binaryPath)
×
109
        .addArgs(args);
×
110
    pc.run();
×
111
  }
×
112

113
  /**
114
   * @return the {@link EnvironmentVariables#getToolEdition(String) tool edition}.
115
   */
116
  protected String getEdition() {
117

118
    return this.context.getVariables().getToolEdition(getName());
7✔
119
  }
120

121
  /**
122
   * @return the {@link #getName() tool} with its {@link #getEdition() edition}. The edition will be omitted if same as
123
   *         tool.
124
   * @see #getToolWithEdition(String, String)
125
   */
126
  protected final String getToolWithEdition() {
127

128
    return getToolWithEdition(getName(), getEdition());
×
129
  }
130

131
  /**
132
   * @param tool the tool name.
133
   * @param edition the edition.
134
   * @return the {@link #getName() tool} with its {@link #getEdition() edition}. The edition will be omitted if same as
135
   *         tool.
136
   */
137
  protected final static String getToolWithEdition(String tool, String edition) {
138

139
    if (tool.equals(edition)) {
×
140
      return tool;
×
141
    }
142
    return tool + "/" + edition;
×
143
  }
144

145
  /**
146
   * @return the {@link EnvironmentVariables#getToolVersion(String) tool version}.
147
   */
148
  public VersionIdentifier getConfiguredVersion() {
149

150
    return this.context.getVariables().getToolVersion(getName());
×
151
  }
152

153
  /**
154
   * Method to be called for {@link #install(boolean)} from dependent {@link Commandlet}s.
155
   *
156
   * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and
157
   *         nothing has changed.
158
   */
159
  public boolean install() {
160

161
    return install(true);
×
162
  }
163

164
  /**
165
   * Performs the installation of the {@link #getName() tool} managed by this {@link Commandlet}.
166
   *
167
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
168
   * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and
169
   *         nothing has changed.
170
   */
171
  public boolean install(boolean silent) {
172

173
    return doInstall(silent);
×
174
  }
175

176
  /**
177
   * Checks if the given {@link VersionIdentifier} has a matching security warning in the {@link UrlSecurityJsonFile}.
178
   *
179
   * @param configuredVersion the {@link VersionIdentifier} to be checked.
180
   * @return the {@link VersionIdentifier} to be used for installation. If the configured version is safe or there are
181
   *         no save versions the potentially unresolved configured version is simply returned. Otherwise, a resolved
182
   *         version is returned.
183
   */
184
  protected VersionIdentifier securityRiskInteraction(VersionIdentifier configuredVersion) {
185

186
    UrlSecurityJsonFile securityFile = this.context.getUrls().getEdition(this.tool, this.getEdition())
8✔
187
        .getSecurityJsonFile();
2✔
188

189
    VersionIdentifier current = this.context.getUrls().getVersion(this.tool, this.getEdition(), configuredVersion);
10✔
190

191
    if (!securityFile.contains(current, true, this.context, securityFile.getParent())) {
10✔
192
      return configuredVersion;
2✔
193
    }
194

195
    List<VersionIdentifier> allVersions = this.context.getUrls().getSortedVersions(this.tool, this.getEdition());
9✔
196
    VersionIdentifier latest = allVersions.get(0);
5✔
197

198
    int currentVersionIndex = allVersions.indexOf(current);
4✔
199
    VersionIdentifier nextSafe = null;
2✔
200
    for (int i = currentVersionIndex - 1; i >= 0; i--) {
8✔
201
      if (!securityFile.contains(allVersions.get(i), true, this.context, securityFile.getParent())) {
13✔
202
        nextSafe = allVersions.get(i);
5✔
203
        break;
1✔
204
      }
205
    }
206
    VersionIdentifier latestSafe = null;
2✔
207
    for (int i = 0; i < allVersions.size(); i++) {
8✔
208
      if (!securityFile.contains(allVersions.get(i), true, this.context, securityFile.getParent())) {
13✔
209
        latestSafe = allVersions.get(i);
5✔
210
        break;
1✔
211
      }
212
    }
213
    String cves = securityFile.getMatchingSecurityWarnings(current).stream().map(UrlSecurityWarning::getCveName)
7✔
214
        .collect(Collectors.joining(", "));
4✔
215
    String currentIsUnsafe = "Currently, version " + current + " of " + this.getName() + " is selected, "
5✔
216
        + "which is has one or more vulnerabilities:\n\n" + cves + "\n\n(See also " + securityFile.getPath() + ")\n\n";
3✔
217

218
    String ask = "Which version do you want to install?";
2✔
219

220
    String stay = "Stay with the current unsafe version (" + current + ").";
3✔
221
    String installLatestSafe = "Install the latest safe version (" + latestSafe + ").";
3✔
222
    String installSafeLatest = "Install the (safe) latest version (" + latest + ").";
3✔
223
    String installNextSafe = "Install the next safe version (" + nextSafe + ").";
3✔
224
    // I don't need to offer "install latest which is unsafe" as option since the user can set to the latest and choose
225
    // "stay"
226

227
    if (latestSafe == null) {
2✔
228
      this.context.warning(currentIsUnsafe + "There is no safe version available.");
5✔
229
      return configuredVersion;
2✔
230
    }
231

232
    if (current.equals(latest)) {
4✔
233
      String answer = this.context.question(currentIsUnsafe + "There are no updates available. " + ask, stay,
18✔
234
          installLatestSafe);
235
      return answer.equals(stay) ? configuredVersion : latestSafe;
8✔
236

237
    } else if (nextSafe == null) { // install an older version that is safe or stay with the current unsafe version
2✔
238
      String answer = this.context.question(currentIsUnsafe + " All newer versions are also not safe. " + ask, stay,
18✔
239
          installLatestSafe);
240
      return answer.equals(stay) ? configuredVersion : latestSafe;
8✔
241

242
    } else if (nextSafe.equals(latest)) {
4✔
243
      String answer = this.context.question(currentIsUnsafe + " Of the newer versions, only the latest is safe. " + ask,
18✔
244
          stay, installSafeLatest);
245
      return answer.equals(stay) ? configuredVersion : VersionIdentifier.LATEST;
8✔
246

247
    } else if (nextSafe.equals(latestSafe)) {
4✔
248
      String answer = this.context.question(
20✔
249
          currentIsUnsafe + " Of the newer versions, only the version " + nextSafe
250
              + " is safe, which is however not the latest." + ask,
251
          stay, "Install the safe version (" + nextSafe + ")");
252
      return answer.equals(stay) ? configuredVersion : nextSafe;
8✔
253

254
    } else {
255
      if (latestSafe.equals(latest)) {
4✔
256
        String answer = this.context.question(currentIsUnsafe + ask, stay, installNextSafe, installSafeLatest);
22✔
257
        return answer.equals(stay) ? configuredVersion
7✔
258
            : answer.equals(installNextSafe) ? nextSafe : VersionIdentifier.LATEST;
7✔
259

260
      } else {
261
        String answer = this.context.question(currentIsUnsafe + ask, stay, installNextSafe, installLatestSafe);
22✔
262
        return answer.equals(stay) ? configuredVersion : answer.equals(installNextSafe) ? nextSafe : latestSafe;
14✔
263
      }
264
    }
265
  }
266

267
  /**
268
   * Installs or updates the managed {@link #getName() tool}.
269
   *
270
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
271
   * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and
272
   *         nothing has changed.
273
   */
274
  protected abstract boolean doInstall(boolean silent);
275

276
  /**
277
   * This method is called after the tool has been newly installed or updated to a new version. Override it to add
278
   * custom post installation logic.
279
   */
280
  protected void postInstall() {
281

282
    // nothing to do by default
283
  }
×
284

285
  /**
286
   * @param path the {@link Path} to start the recursive search from.
287
   * @return the deepest subdir {@code s} of the passed path such that all directories between {@code s} and the passed
288
   *         path (including {@code s}) are the sole item in their respective directory and {@code s} is not named
289
   *         "bin".
290
   */
291
  private Path getProperInstallationSubDirOf(Path path) {
292

293
    try (Stream<Path> stream = Files.list(path)) {
×
294
      Path[] subFiles = stream.toArray(Path[]::new);
×
295
      if (subFiles.length == 0) {
×
296
        throw new CliException("The downloaded package for the tool " + this.tool
×
297
            + " seems to be empty as you can check in the extracted folder " + path);
298
      } else if (subFiles.length == 1) {
×
299
        String filename = subFiles[0].getFileName().toString();
×
300
        if (!filename.equals(IdeContext.FOLDER_BIN) && !filename.equals(IdeContext.FOLDER_CONTENTS)
×
301
            && !filename.endsWith(".app") && Files.isDirectory(subFiles[0])) {
×
302
          return getProperInstallationSubDirOf(subFiles[0]);
×
303
        }
304
      }
305
      return path;
×
306
    } catch (IOException e) {
×
307
      throw new IllegalStateException("Failed to get sub-files of " + path);
×
308
    }
309
  }
310

311
  /**
312
   * @param file the {@link Path} to the file to extract.
313
   * @param targetDir the {@link Path} to the directory where to extract (or copy) the file.
314
   */
315
  protected void extract(Path file, Path targetDir) {
316

317
    FileAccess fileAccess = this.context.getFileAccess();
×
318
    if (isExtract()) {
×
319
      Path tmpDir = this.context.getFileAccess().createTempDir("extract-" + file.getFileName());
×
320
      this.context.trace("Trying to extract the downloaded file {} to {} and move it to {}.", file, tmpDir, targetDir);
×
321
      String extension = FilenameUtil.getExtension(file.getFileName().toString());
×
322
      this.context.trace("Determined file extension {}", extension);
×
323
      TarCompression tarCompression = TarCompression.of(extension);
×
324
      if (tarCompression != null) {
×
325
        fileAccess.untar(file, tmpDir, tarCompression);
×
326
      } else if ("zip".equals(extension) || "jar".equals(extension)) {
×
327
        fileAccess.unzip(file, tmpDir);
×
328
      } else if ("dmg".equals(extension)) {
×
329
        assert this.context.getSystemInfo().isMac();
×
330
        Path mountPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES).resolve(IdeContext.FOLDER_VOLUME);
×
331
        fileAccess.mkdirs(mountPath);
×
332
        ProcessContext pc = this.context.newProcess();
×
333
        pc.executable("hdiutil");
×
334
        pc.addArgs("attach", "-quiet", "-nobrowse", "-mountpoint", mountPath, file);
×
335
        pc.run();
×
336
        Path appPath = fileAccess.findFirst(mountPath, p -> p.getFileName().toString().endsWith(".app"), false);
×
337
        if (appPath == null) {
×
338
          throw new IllegalStateException("Failed to unpack DMG as no MacOS *.app was found in file " + file);
×
339
        }
340
        fileAccess.copy(appPath, tmpDir);
×
341
        pc.addArgs("detach", "-force", mountPath);
×
342
        pc.run();
×
343
        // if [ -e "${target_dir}/Applications" ]
344
        // then
345
        // rm "${target_dir}/Applications"
346
        // fi
347
      } else if ("msi".equals(extension)) {
×
348
        this.context.newProcess().executable("msiexec").addArgs("/a", file, "/qn", "TARGETDIR=" + tmpDir).run();
×
349
        // msiexec also creates a copy of the MSI
350
        Path msiCopy = tmpDir.resolve(file.getFileName());
×
351
        fileAccess.delete(msiCopy);
×
352
      } else if ("pkg".equals(extension)) {
×
353

354
        Path tmpDirPkg = fileAccess.createTempDir("ide-pkg-");
×
355
        ProcessContext pc = this.context.newProcess();
×
356
        // we might also be able to use cpio from commons-compression instead of external xar...
357
        pc.executable("xar").addArgs("-C", tmpDirPkg, "-xf", file).run();
×
358
        Path contentPath = fileAccess.findFirst(tmpDirPkg, p -> p.getFileName().toString().equals("Payload"), true);
×
359
        fileAccess.untar(contentPath, tmpDir, TarCompression.GZ);
×
360
        fileAccess.delete(tmpDirPkg);
×
361
      } else {
×
362
        throw new IllegalStateException("Unknown archive format " + extension + ". Can not extract " + file);
×
363
      }
364
      fileAccess.move(getProperInstallationSubDirOf(tmpDir), targetDir);
×
365
      fileAccess.delete(tmpDir);
×
366
    } else {
×
367
      this.context.trace("Extraction is disabled for '{}' hence just moving the downloaded file {}.", getName(), file);
×
368
      fileAccess.move(file, targetDir);
×
369
    }
370
  }
×
371

372
  /**
373
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
374
   */
375
  protected boolean isExtract() {
376

377
    return true;
×
378
  }
379

380
  /**
381
   * @return the {@link MacOsHelper} instance.
382
   */
383
  protected MacOsHelper getMacOsHelper() {
384

385
    if (this.macOsHelper == null) {
×
386
      this.macOsHelper = new MacOsHelper(this.context);
×
387
    }
388
    return this.macOsHelper;
×
389
  }
390

391
  /**
392
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
393
   */
394
  public VersionIdentifier getInstalledVersion() {
395

396
    return getInstalledVersion(this.context.getSoftwarePath().resolve(getName()));
9✔
397
  }
398

399
  /**
400
   * @param toolPath the installation {@link Path} where to find the version file.
401
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
402
   */
403
  protected VersionIdentifier getInstalledVersion(Path toolPath) {
404

405
    if (!Files.isDirectory(toolPath)) {
5!
406
      this.context.debug("Tool {} not installed in {}", getName(), toolPath);
×
407
      return null;
×
408
    }
409
    Path toolVersionFile = toolPath.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
410
    if (!Files.exists(toolVersionFile)) {
5!
411
      Path legacyToolVersionFile = toolPath.resolve(IdeContext.FILE_LEGACY_SOFTWARE_VERSION);
4✔
412
      if (Files.exists(legacyToolVersionFile)) {
5✔
413
        toolVersionFile = legacyToolVersionFile;
3✔
414
      } else {
415
        this.context.warning("Tool {} is missing version file in {}", getName(), toolVersionFile);
15✔
416
        return null;
2✔
417
      }
418
    }
419
    try {
420
      String version = Files.readString(toolVersionFile).trim();
4✔
421
      return VersionIdentifier.of(version);
3✔
422
    } catch (IOException e) {
×
423
      throw new IllegalStateException("Failed to read file " + toolVersionFile, e);
×
424
    }
425
  }
426

427
  /**
428
   * List the available versions of this tool.
429
   */
430
  public void listVersions() {
431

432
    List<VersionIdentifier> versions = this.context.getUrls().getSortedVersions(getName(), getEdition());
9✔
433
    for (VersionIdentifier vi : versions) {
10✔
434
      this.context.info(vi.toString());
5✔
435
    }
1✔
436
  }
1✔
437

438
  /**
439
   * Sets the tool version in the environment variable configuration file.
440
   *
441
   * @param version the version (pattern) to set.
442
   */
443
  public void setVersion(String version) {
444

445
    if ((version == null) || version.isBlank()) {
×
446
      throw new IllegalStateException("Version has to be specified!");
×
447
    }
448
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
449
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
450
      this.context.warning("Version {} seems to be invalid", version);
×
451
    }
452
    setVersion(configuredVersion, true);
×
453
  }
×
454

455
  /**
456
   * Sets the tool version in the environment variable configuration file.
457
   *
458
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
459
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
460
   */
461
  public void setVersion(VersionIdentifier version, boolean hint) {
462

463
    EnvironmentVariables variables = this.context.getVariables();
4✔
464
    EnvironmentVariables settingsVariables = variables.getByType(EnvironmentVariablesType.SETTINGS);
4✔
465
    String edition = getEdition();
3✔
466
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
467
    VersionIdentifier resolvedVersion = this.context.getUrls().getVersion(this.tool, edition, version);
9✔
468
    if (version.isPattern()) {
3!
469
      this.context.debug("Resolved version {} to {} for tool {}/{}", version, resolvedVersion, this.tool, edition);
×
470
    }
471
    settingsVariables.set(name, resolvedVersion.toString(), false);
7✔
472
    settingsVariables.save();
2✔
473
    this.context.info("{}={} has been set in {}", name, version, settingsVariables.getSource());
19✔
474
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
475
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
476
      this.context.warning(
×
477
          "The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.",
478
          name, declaringVariables.getSource());
×
479
    }
480
    if (hint) {
2!
481
      this.context.info("To install that version call the following command:");
4✔
482
      this.context.info("ide install {}", this.tool);
11✔
483
    }
484
  }
1✔
485

486
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc