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

devonfw / IDEasy / 17731348411

15 Sep 2025 11:22AM UTC coverage: 68.544% (-0.08%) from 68.627%
17731348411

Pull #1463

github

web-flow
Merge 706fb946f into 2efad3d2e
Pull Request #1463: #1454: Add ng commandlet

3413 of 5447 branches covered (62.66%)

Branch coverage included in aggregate %.

8901 of 12518 relevant lines covered (71.11%)

3.12 hits per line

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

66.73
cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
1
package com.devonfw.tools.ide.io;
2

3
import java.io.BufferedOutputStream;
4
import java.io.FileOutputStream;
5
import java.io.IOException;
6
import java.io.InputStream;
7
import java.io.OutputStream;
8
import java.io.Reader;
9
import java.io.Writer;
10
import java.net.URI;
11
import java.net.http.HttpClient;
12
import java.net.http.HttpClient.Redirect;
13
import java.net.http.HttpClient.Version;
14
import java.net.http.HttpRequest;
15
import java.net.http.HttpRequest.Builder;
16
import java.net.http.HttpResponse;
17
import java.nio.file.FileSystem;
18
import java.nio.file.FileSystemException;
19
import java.nio.file.FileSystems;
20
import java.nio.file.Files;
21
import java.nio.file.LinkOption;
22
import java.nio.file.NoSuchFileException;
23
import java.nio.file.Path;
24
import java.nio.file.Paths;
25
import java.nio.file.StandardCopyOption;
26
import java.nio.file.attribute.BasicFileAttributes;
27
import java.nio.file.attribute.DosFileAttributeView;
28
import java.nio.file.attribute.FileTime;
29
import java.nio.file.attribute.PosixFileAttributeView;
30
import java.nio.file.attribute.PosixFilePermission;
31
import java.security.DigestInputStream;
32
import java.security.MessageDigest;
33
import java.security.NoSuchAlgorithmException;
34
import java.time.Duration;
35
import java.time.LocalDateTime;
36
import java.util.ArrayList;
37
import java.util.HashSet;
38
import java.util.Iterator;
39
import java.util.List;
40
import java.util.Map;
41
import java.util.Properties;
42
import java.util.Set;
43
import java.util.function.Consumer;
44
import java.util.function.Function;
45
import java.util.function.Predicate;
46
import java.util.stream.Stream;
47

48
import org.apache.commons.compress.archivers.ArchiveEntry;
49
import org.apache.commons.compress.archivers.ArchiveInputStream;
50
import org.apache.commons.compress.archivers.ArchiveOutputStream;
51
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
52
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
53
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
54
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
55
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
56
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
57
import org.apache.commons.io.IOUtils;
58

59
import com.devonfw.tools.ide.cli.CliException;
60
import com.devonfw.tools.ide.cli.CliOfflineException;
61
import com.devonfw.tools.ide.context.IdeContext;
62
import com.devonfw.tools.ide.io.ini.IniComment;
63
import com.devonfw.tools.ide.io.ini.IniFile;
64
import com.devonfw.tools.ide.io.ini.IniSection;
65
import com.devonfw.tools.ide.os.SystemInfoImpl;
66
import com.devonfw.tools.ide.process.ProcessContext;
67
import com.devonfw.tools.ide.process.ProcessMode;
68
import com.devonfw.tools.ide.process.ProcessResult;
69
import com.devonfw.tools.ide.util.DateTimeUtil;
70
import com.devonfw.tools.ide.util.FilenameUtil;
71
import com.devonfw.tools.ide.util.HexUtil;
72
import com.devonfw.tools.ide.variable.IdeVariables;
73

74
/**
75
 * Implementation of {@link FileAccess}.
76
 */
77
public class FileAccessImpl implements FileAccess {
78

79
  private static final String WINDOWS_FILE_LOCK_DOCUMENTATION_PAGE = "https://github.com/devonfw/IDEasy/blob/main/documentation/windows-file-lock.adoc";
80

81
  private static final String WINDOWS_FILE_LOCK_WARNING =
82
      "On Windows, file operations could fail due to file locks. Please ensure the files in the moved directory are not in use. For further details, see: \n"
83
          + WINDOWS_FILE_LOCK_DOCUMENTATION_PAGE;
84

85
  private static final int MODE_RWX_RX_RX = 0755;
86
  private static final int MODE_RW_R_R = 0644;
87

88
  private static final Map<String, String> FS_ENV = Map.of("encoding", "UTF-8");
5✔
89

90
  private final IdeContext context;
91

92
  /**
93
   * The constructor.
94
   *
95
   * @param context the {@link IdeContext} to use.
96
   */
97
  public FileAccessImpl(IdeContext context) {
98

99
    super();
2✔
100
    this.context = context;
3✔
101
  }
1✔
102

103
  private HttpClient createHttpClient(String url) {
104

105
    return HttpClient.newBuilder().followRedirects(Redirect.ALWAYS).build();
5✔
106
  }
107

108
  @Override
109
  public void download(String url, Path target) {
110

111
    if (this.context.isOffline()) {
4!
112
      throw CliOfflineException.ofDownloadViaUrl(url);
×
113
    }
114
    if (url.startsWith("http")) {
4✔
115
      downloadViaHttp(url, target);
5✔
116
    } else if (url.startsWith("ftp") || url.startsWith("sftp")) {
8!
117
      throw new IllegalArgumentException("Unsupported download URL: " + url);
×
118
    } else {
119
      Path source = Path.of(url);
5✔
120
      if (isFile(source)) {
4!
121
        // network drive
122
        copyFileWithProgressBar(source, target);
5✔
123
      } else {
124
        throw new IllegalArgumentException("Download path does not point to a downloadable file: " + url);
×
125
      }
126
    }
127
  }
1✔
128

129
  private void downloadViaHttp(String url, Path target) {
130
    List<Version> httpProtocols = IdeVariables.HTTP_VERSIONS.get(context);
6✔
131
    Exception lastException = null;
2✔
132
    if (httpProtocols.isEmpty()) {
3!
133
      try {
134
        this.downloadWithHttpVersion(url, target, null);
5✔
135
        return;
1✔
136
      } catch (Exception e) {
×
137
        lastException = e;
×
138
      }
×
139
    } else {
140
      for (Version version : httpProtocols) {
×
141
        try {
142
          this.downloadWithHttpVersion(url, target, version);
×
143
          return;
×
144
        } catch (Exception ex) {
×
145
          lastException = ex;
×
146
        }
147
      }
×
148
    }
149
    throw new IllegalStateException("Failed to download file from URL " + url + " to " + target, lastException);
×
150
  }
151

152
  private void downloadWithHttpVersion(String url, Path target, Version httpVersion) throws Exception {
153

154
    if (httpVersion == null) {
2!
155
      this.context.info("Trying to download {} from {}", target.getFileName(), url);
16✔
156
    } else {
157
      this.context.info("Trying to download: {} with HTTP protocol version: {}", url, httpVersion);
×
158
    }
159
    mkdirs(target.getParent());
4✔
160

161
    Builder builder = HttpRequest.newBuilder()
2✔
162
        .uri(URI.create(url))
2✔
163
        .GET();
2✔
164
    if (httpVersion != null) {
2!
165
      builder.version(httpVersion);
×
166
    }
167
    try (HttpClient client = createHttpClient(url)) {
4✔
168
      HttpRequest request = builder.build();
3✔
169
      HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
5✔
170
      int statusCode = response.statusCode();
3✔
171
      if (statusCode == 200) {
3!
172
        downloadFileWithProgressBar(url, target, response);
6✔
173
      } else {
174
        throw new IllegalStateException("Download failed with status code " + statusCode);
×
175
      }
176
    }
177
  }
1✔
178

179
  /**
180
   * Downloads a file while showing a {@link IdeProgressBar}.
181
   *
182
   * @param url the url to download.
183
   * @param target Path of the target directory.
184
   * @param response the {@link HttpResponse} to use.
185
   */
186
  private void downloadFileWithProgressBar(String url, Path target, HttpResponse<InputStream> response) {
187

188
    long contentLength = response.headers().firstValueAsLong("content-length").orElse(-1);
7✔
189
    informAboutMissingContentLength(contentLength, url);
4✔
190

191
    byte[] data = new byte[1024];
3✔
192
    boolean fileComplete = false;
2✔
193
    int count;
194

195
    try (InputStream body = response.body();
4✔
196
        FileOutputStream fileOutput = new FileOutputStream(target.toFile());
6✔
197
        BufferedOutputStream bufferedOut = new BufferedOutputStream(fileOutput, data.length);
7✔
198
        IdeProgressBar pb = this.context.newProgressBarForDownload(contentLength)) {
5✔
199
      while (!fileComplete) {
2✔
200
        count = body.read(data);
4✔
201
        if (count <= 0) {
2✔
202
          fileComplete = true;
3✔
203
        } else {
204
          bufferedOut.write(data, 0, count);
5✔
205
          pb.stepBy(count);
5✔
206
        }
207
      }
208

209
    } catch (Exception e) {
×
210
      throw new RuntimeException(e);
×
211
    }
1✔
212
  }
1✔
213

214
  private void copyFileWithProgressBar(Path source, Path target) {
215

216
    long size = getFileSize(source);
4✔
217
    if (size < 100_000) {
4✔
218
      copy(source, target, FileCopyMode.COPY_FILE_TO_TARGET_OVERRIDE);
5✔
219
      return;
1✔
220
    }
221
    try (InputStream in = Files.newInputStream(source);
5✔
222
        OutputStream out = Files.newOutputStream(target)) {
5✔
223
      byte[] buf = new byte[1024];
3✔
224
      try (IdeProgressBar pb = this.context.newProgressbarForCopying(size)) {
5✔
225
        int readBytes;
226
        while ((readBytes = in.read(buf)) > 0) {
6✔
227
          out.write(buf, 0, readBytes);
5✔
228
          pb.stepBy(readBytes);
5✔
229
        }
230
      } catch (Exception e) {
×
231
        throw new RuntimeException(e);
×
232
      }
1✔
233
    } catch (IOException e) {
×
234
      throw new RuntimeException("Failed to copy from " + source + " to " + target, e);
×
235
    }
1✔
236
  }
1✔
237

238
  private void informAboutMissingContentLength(long contentLength, String url) {
239

240
    if (contentLength < 0) {
4✔
241
      this.context.warning("Content-Length was not provided by download from {}", url);
10✔
242
    }
243
  }
1✔
244

245
  @Override
246
  public void mkdirs(Path directory) {
247

248
    if (Files.isDirectory(directory)) {
5✔
249
      return;
1✔
250
    }
251
    this.context.trace("Creating directory {}", directory);
10✔
252
    try {
253
      Files.createDirectories(directory);
5✔
254
    } catch (IOException e) {
×
255
      throw new IllegalStateException("Failed to create directory " + directory, e);
×
256
    }
1✔
257
  }
1✔
258

259
  @Override
260
  public boolean isFile(Path file) {
261

262
    if (!Files.exists(file)) {
5!
263
      this.context.trace("File {} does not exist", file);
×
264
      return false;
×
265
    }
266
    if (Files.isDirectory(file)) {
5!
267
      this.context.trace("Path {} is a directory but a regular file was expected", file);
×
268
      return false;
×
269
    }
270
    return true;
2✔
271
  }
272

273
  @Override
274
  public boolean isExpectedFolder(Path folder) {
275

276
    if (Files.isDirectory(folder)) {
5✔
277
      return true;
2✔
278
    }
279
    this.context.warning("Expected folder was not found at {}", folder);
10✔
280
    return false;
2✔
281
  }
282

283
  @Override
284
  public String checksum(Path file, String hashAlgorithm) {
285

286
    MessageDigest md;
287
    try {
288
      md = MessageDigest.getInstance(hashAlgorithm);
×
289
    } catch (NoSuchAlgorithmException e) {
×
290
      throw new IllegalStateException("No such hash algorithm " + hashAlgorithm, e);
×
291
    }
×
292
    byte[] buffer = new byte[1024];
×
293
    try (InputStream is = Files.newInputStream(file); DigestInputStream dis = new DigestInputStream(is, md)) {
×
294
      int read = 0;
×
295
      while (read >= 0) {
×
296
        read = dis.read(buffer);
×
297
      }
298
    } catch (Exception e) {
×
299
      throw new IllegalStateException("Failed to read and hash file " + file, e);
×
300
    }
×
301
    byte[] digestBytes = md.digest();
×
302
    return HexUtil.toHexString(digestBytes);
×
303
  }
304

305
  @Override
306
  public boolean isJunction(Path path) {
307

308
    if (!SystemInfoImpl.INSTANCE.isWindows()) {
3!
309
      return false;
2✔
310
    }
311
    try {
312
      BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
×
313
      return attr.isOther() && attr.isDirectory();
×
314
    } catch (NoSuchFileException e) {
×
315
      return false; // file doesn't exist
×
316
    } catch (IOException e) {
×
317
      // errors in reading the attributes of the file
318
      throw new IllegalStateException("An unexpected error occurred whilst checking if the file: " + path + " is a junction", e);
×
319
    }
320
  }
321

322
  @Override
323
  public Path backup(Path fileOrFolder) {
324

325
    if ((fileOrFolder != null) && (Files.isSymbolicLink(fileOrFolder) || isJunction(fileOrFolder))) {
9!
326
      delete(fileOrFolder);
4✔
327
    } else if ((fileOrFolder != null) && Files.exists(fileOrFolder)) {
7!
328
      LocalDateTime now = LocalDateTime.now();
2✔
329
      String date = DateTimeUtil.formatDate(now, true);
4✔
330
      String time = DateTimeUtil.formatTime(now);
3✔
331
      String filename = fileOrFolder.getFileName().toString();
4✔
332
      Path backupPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_BACKUPS).resolve(date).resolve(time + "_" + filename);
12✔
333
      backupPath = appendParentPath(backupPath, fileOrFolder.getParent(), 2);
6✔
334
      mkdirs(backupPath);
3✔
335
      Path target = backupPath.resolve(filename);
4✔
336
      this.context.info("Creating backup by moving {} to {}", fileOrFolder, target);
14✔
337
      move(fileOrFolder, target);
6✔
338
      return target;
2✔
339
    } else {
340
      this.context.trace("Backup of {} skipped as the path does not exist.", fileOrFolder);
10✔
341
    }
342
    return fileOrFolder;
2✔
343
  }
344

345
  private static Path appendParentPath(Path path, Path parent, int max) {
346

347
    if ((parent == null) || (max <= 0)) {
4!
348
      return path;
2✔
349
    }
350
    return appendParentPath(path, parent.getParent(), max - 1).resolve(parent.getFileName());
11✔
351
  }
352

353
  @Override
354
  public void move(Path source, Path targetDir, StandardCopyOption... copyOptions) {
355

356
    this.context.trace("Moving {} to {}", source, targetDir);
14✔
357
    try {
358
      Files.move(source, targetDir, copyOptions);
5✔
359
    } catch (IOException e) {
×
360
      String fileType = Files.isSymbolicLink(source) ? "symlink" : isJunction(source) ? "junction" : Files.isDirectory(source) ? "directory" : "file";
×
361
      String message = "Failed to move " + fileType + ": " + source + " to " + targetDir + ".";
×
362
      if (this.context.getSystemInfo().isWindows()) {
×
363
        message = message + "\n" + WINDOWS_FILE_LOCK_WARNING;
×
364
      }
365
      throw new IllegalStateException(message, e);
×
366
    }
1✔
367
  }
1✔
368

369
  @Override
370
  public void copy(Path source, Path target, FileCopyMode mode, PathCopyListener listener) {
371

372
    if (mode.isUseSourceFilename()) {
3✔
373
      // if we want to copy the file or folder "source" to the existing folder "target" in a shell this will copy
374
      // source into that folder so that we as a result have a copy in "target/source".
375
      // With Java NIO the raw copy method will fail as we cannot copy "source" to the path of the "target" folder.
376
      // For folders we want the same behavior as the linux "cp -r" command so that the "source" folder is copied
377
      // and not only its content what also makes it consistent with the move method that also behaves this way.
378
      // Therefore we need to add the filename (foldername) of "source" to the "target" path before.
379
      // For the rare cases, where we want to copy the content of a folder (cp -r source/* target) we support
380
      // it via the COPY_TREE_CONTENT mode.
381
      Path fileName = source.getFileName();
3✔
382
      if (fileName != null) { // if filename is null, we are copying the root of a (virtual filesystem)
2✔
383
        target = target.resolve(fileName.toString());
5✔
384
      }
385
    }
386
    boolean fileOnly = mode.isFileOnly();
3✔
387
    String operation = mode.getOperation();
3✔
388
    if (mode.isExtract()) {
3✔
389
      this.context.debug("Starting to {} to {}", operation, target);
15✔
390
    } else {
391
      if (fileOnly) {
2✔
392
        this.context.debug("Starting to {} file {} to {}", operation, source, target);
19✔
393
      } else {
394
        this.context.debug("Starting to {} {} recursively to {}", operation, source, target);
18✔
395
      }
396
    }
397
    if (fileOnly && Files.isDirectory(source)) {
7!
398
      throw new IllegalStateException("Expected file but found a directory to copy at " + source);
×
399
    }
400
    if (mode.isFailIfExists()) {
3✔
401
      if (Files.exists(target)) {
5!
402
        throw new IllegalStateException("Failed to " + operation + " " + source + " to already existing target " + target);
×
403
      }
404
    } else if (mode == FileCopyMode.COPY_TREE_OVERRIDE_TREE) {
3✔
405
      delete(target);
3✔
406
    }
407
    try {
408
      copyRecursive(source, target, mode, listener);
6✔
409
    } catch (IOException e) {
×
410
      throw new IllegalStateException("Failed to " + operation + " " + source + " to " + target, e);
×
411
    }
1✔
412
  }
1✔
413

414
  private void copyRecursive(Path source, Path target, FileCopyMode mode, PathCopyListener listener) throws IOException {
415

416
    if (Files.isDirectory(source)) {
5✔
417
      mkdirs(target);
3✔
418
      try (Stream<Path> childStream = Files.list(source)) {
3✔
419
        Iterator<Path> iterator = childStream.iterator();
3✔
420
        while (iterator.hasNext()) {
3✔
421
          Path child = iterator.next();
4✔
422
          copyRecursive(child, target.resolve(child.getFileName().toString()), mode, listener);
10✔
423
        }
1✔
424
      }
425
      listener.onCopy(source, target, true);
6✔
426
    } else if (Files.exists(source)) {
5!
427
      if (mode.isOverride()) {
3✔
428
        delete(target);
3✔
429
      }
430
      this.context.trace("Starting to {} {} to {}", mode.getOperation(), source, target);
19✔
431
      Files.copy(source, target);
6✔
432
      listener.onCopy(source, target, false);
6✔
433
    } else {
434
      throw new IOException("Path " + source + " does not exist.");
×
435
    }
436
  }
1✔
437

438
  /**
439
   * Deletes the given {@link Path} if it is a symbolic link or a Windows junction. And throws an {@link IllegalStateException} if there is a file at the given
440
   * {@link Path} that is neither a symbolic link nor a Windows junction.
441
   *
442
   * @param path the {@link Path} to delete.
443
   */
444
  private void deleteLinkIfExists(Path path) {
445

446
    boolean isJunction = isJunction(path); // since broken junctions are not detected by Files.exists()
4✔
447
    boolean isSymlink = Files.exists(path) && Files.isSymbolicLink(path);
12!
448

449
    assert !(isSymlink && isJunction);
5!
450

451
    if (isJunction || isSymlink) {
4!
452
      this.context.info("Deleting previous " + (isJunction ? "junction" : "symlink") + " at " + path);
9!
453
      try {
454
        Files.delete(path);
2✔
455
      } catch (IOException e) {
×
456
        throw new IllegalStateException("Failed to delete link at " + path, e);
×
457
      }
1✔
458
    }
459
  }
1✔
460

461
  /**
462
   * Adapts the given {@link Path} to be relative or absolute depending on the given {@code relative} flag. Additionally, {@link Path#toRealPath(LinkOption...)}
463
   * is applied to {@code target}.
464
   *
465
   * @param target the {@link Path} the link should point to and that is to be adapted.
466
   * @param link the {@link Path} to the link. It is used to calculate the relative path to the {@code target} if {@code relative} is set to {@code true}.
467
   * @param relative the {@code relative} flag.
468
   * @return the adapted {@link Path}.
469
   * @see #symlink(Path, Path, boolean)
470
   */
471
  private Path adaptPath(Path target, Path link, boolean relative) {
472

473
    if (!target.isAbsolute()) {
3✔
474
      target = link.resolveSibling(target);
4✔
475
    }
476
    try {
477
      target = target.toRealPath(LinkOption.NOFOLLOW_LINKS); // to transform ../d1/../d2 to ../d2
9✔
478
    } catch (IOException e) {
×
479
      throw new RuntimeException("Failed to get real path of " + target, e);
×
480
    }
1✔
481
    if (relative) {
2✔
482
      target = link.getParent().relativize(target);
5✔
483
      // to make relative links like this work: dir/link -> dir
484
      target = (target.toString().isEmpty()) ? Path.of(".") : target;
11✔
485
    }
486
    return target;
2✔
487
  }
488

489
  /**
490
   * Creates a Windows link using mklink at {@code link} pointing to {@code target}.
491
   *
492
   * @param target the {@link Path} the link will point to.
493
   * @param link the {@link Path} where to create the link.
494
   * @param type the {@link PathLinkType}.
495
   */
496
  private void mklinkOnWindows(Path target, Path link, PathLinkType type) {
497

498
    this.context.trace("Creating a Windows link at {} pointing to {}", link, target);
×
499
    ProcessResult result = this.context.newProcess().executable("cmd").addArgs("/c", "mklink", type.getMklinkOption(), link.toString(), target.toString())
×
500
        .run(ProcessMode.DEFAULT);
×
501
    result.failOnError();
×
502
  }
×
503

504
  @Override
505
  public void link(Path target, Path link, boolean relative, PathLinkType type) {
506

507
    final Path finalTarget;
508
    try {
509
      finalTarget = adaptPath(target, link, relative);
6✔
510
    } catch (Exception e) {
×
511
      throw new IllegalStateException("Failed to adapt target (" + target + ") for link (" + link + ") and relative (" + relative + ")", e);
×
512
    }
1✔
513
    String relativeOrAbsolute = finalTarget.isAbsolute() ? "absolute" : "relative";
7✔
514
    this.context.debug("Creating {} {} at {} pointing to {}", relativeOrAbsolute, type, link, finalTarget);
22✔
515
    deleteLinkIfExists(link);
3✔
516
    try {
517
      if (type == PathLinkType.SYMBOLIC_LINK) {
3!
518
        Files.createSymbolicLink(link, finalTarget);
7✔
519
      } else if (type == PathLinkType.HARD_LINK) {
×
520
        Files.createLink(link, finalTarget);
×
521
      } else {
522
        throw new IllegalStateException("" + type);
×
523
      }
524
    } catch (FileSystemException e) {
×
525
      if (SystemInfoImpl.INSTANCE.isWindows()) {
×
526
        this.context.info(
×
527
            "Due to lack of permissions, Microsoft's mklink with junction had to be used to create a Symlink. See\n"
528
                + "https://github.com/devonfw/IDEasy/blob/main/documentation/symlink.adoc for further details. Error was: "
529
                + e.getMessage());
×
530
        mklinkOnWindows(finalTarget, link, type);
×
531
      } else {
532
        throw new RuntimeException(e);
×
533
      }
534
    } catch (IOException e) {
×
535
      throw new IllegalStateException(
×
536
          "Failed to create a " + relativeOrAbsolute + " " + type + " at " + link + " pointing to " + target, e);
537
    }
1✔
538
  }
1✔
539

540
  @Override
541
  public Path toRealPath(Path path) {
542

543
    return toRealPath(path, true);
5✔
544
  }
545

546
  @Override
547
  public Path toCanonicalPath(Path path) {
548

549
    return toRealPath(path, false);
5✔
550
  }
551

552
  private Path toRealPath(Path path, boolean resolveLinks) {
553

554
    try {
555
      Path realPath;
556
      if (resolveLinks) {
2✔
557
        realPath = path.toRealPath();
6✔
558
      } else {
559
        realPath = path.toRealPath(LinkOption.NOFOLLOW_LINKS);
9✔
560
      }
561
      if (!realPath.equals(path)) {
4✔
562
        this.context.trace("Resolved path {} to {}", path, realPath);
14✔
563
      }
564
      return realPath;
2✔
565
    } catch (IOException e) {
×
566
      throw new IllegalStateException("Failed to get real path for " + path, e);
×
567
    }
568
  }
569

570
  @Override
571
  public Path createTempDir(String name) {
572

573
    try {
574
      Path tmp = this.context.getTempPath();
4✔
575
      Path tempDir = tmp.resolve(name);
4✔
576
      int tries = 1;
2✔
577
      while (Files.exists(tempDir)) {
5!
578
        long id = System.nanoTime() & 0xFFFF;
×
579
        tempDir = tmp.resolve(name + "-" + id);
×
580
        tries++;
×
581
        if (tries > 200) {
×
582
          throw new IOException("Unable to create unique name!");
×
583
        }
584
      }
×
585
      return Files.createDirectory(tempDir);
5✔
586
    } catch (IOException e) {
×
587
      throw new IllegalStateException("Failed to create temporary directory with prefix '" + name + "'!", e);
×
588
    }
589
  }
590

591
  @Override
592
  public void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtractHook, boolean extract) {
593

594
    if (Files.isDirectory(archiveFile)) {
5✔
595
      this.context.warning("Found directory for download at {} hence copying without extraction!", archiveFile);
10✔
596
      copy(archiveFile, targetDir, FileCopyMode.COPY_TREE_CONTENT);
5✔
597
      postExtractHook(postExtractHook, targetDir);
4✔
598
      return;
1✔
599
    } else if (!extract) {
2✔
600
      mkdirs(targetDir);
3✔
601
      move(archiveFile, targetDir.resolve(archiveFile.getFileName()));
9✔
602
      return;
1✔
603
    }
604
    Path tmpDir = createTempDir("extract-" + archiveFile.getFileName());
7✔
605
    this.context.trace("Trying to extract the downloaded file {} to {} and move it to {}.", archiveFile, tmpDir, targetDir);
18✔
606
    String filename = archiveFile.getFileName().toString();
4✔
607
    TarCompression tarCompression = TarCompression.of(filename);
3✔
608
    if (tarCompression != null) {
2✔
609
      extractTar(archiveFile, tmpDir, tarCompression);
6✔
610
    } else {
611
      String extension = FilenameUtil.getExtension(filename);
3✔
612
      if (extension == null) {
2!
613
        throw new IllegalStateException("Unknown archive format without extension - can not extract " + archiveFile);
×
614
      } else {
615
        this.context.trace("Determined file extension {}", extension);
10✔
616
      }
617
      switch (extension) {
8!
618
        case "zip" -> extractZip(archiveFile, tmpDir);
×
619
        case "jar" -> extractJar(archiveFile, tmpDir);
5✔
620
        case "dmg" -> extractDmg(archiveFile, tmpDir);
×
621
        case "msi" -> extractMsi(archiveFile, tmpDir);
×
622
        case "pkg" -> extractPkg(archiveFile, tmpDir);
×
623
        default -> throw new IllegalStateException("Unknown archive format " + extension + ". Can not extract " + archiveFile);
×
624
      }
625
    }
626
    Path properInstallDir = getProperInstallationSubDirOf(tmpDir, archiveFile);
5✔
627
    postExtractHook(postExtractHook, properInstallDir);
4✔
628
    move(properInstallDir, targetDir);
6✔
629
    delete(tmpDir);
3✔
630
  }
1✔
631

632
  private void postExtractHook(Consumer<Path> postExtractHook, Path properInstallDir) {
633

634
    if (postExtractHook != null) {
2✔
635
      postExtractHook.accept(properInstallDir);
3✔
636
    }
637
  }
1✔
638

639
  /**
640
   * @param path the {@link Path} to start the recursive search from.
641
   * @return the deepest subdir {@code s} of the passed path such that all directories between {@code s} and the passed path (including {@code s}) are the sole
642
   *     item in their respective directory and {@code s} is not named "bin".
643
   */
644
  private Path getProperInstallationSubDirOf(Path path, Path archiveFile) {
645

646
    try (Stream<Path> stream = Files.list(path)) {
3✔
647
      Path[] subFiles = stream.toArray(Path[]::new);
8✔
648
      if (subFiles.length == 0) {
3!
649
        throw new CliException("The downloaded package " + archiveFile + " seems to be empty as you can check in the extracted folder " + path);
×
650
      } else if (subFiles.length == 1) {
4✔
651
        String filename = subFiles[0].getFileName().toString();
6✔
652
        if (!filename.equals(IdeContext.FOLDER_BIN) && !filename.equals(IdeContext.FOLDER_CONTENTS) && !filename.endsWith(".app") && Files.isDirectory(
4!
653
            subFiles[0])) {
654
          return getProperInstallationSubDirOf(subFiles[0], archiveFile);
×
655
        }
656
      }
657
      return path;
4✔
658
    } catch (IOException e) {
×
659
      throw new IllegalStateException("Failed to get sub-files of " + path);
×
660
    }
661
  }
662

663
  @Override
664
  public void extractZip(Path file, Path targetDir) {
665

666
    this.context.info("Extracting ZIP file {} to {}", file, targetDir);
14✔
667
    URI uri = URI.create("jar:" + file.toUri());
6✔
668
    try (FileSystem fs = FileSystems.newFileSystem(uri, FS_ENV)) {
4✔
669
      long size = 0;
2✔
670
      for (Path root : fs.getRootDirectories()) {
11✔
671
        size += getFileSizeRecursive(root);
6✔
672
      }
1✔
673
      try (final IdeProgressBar progressBar = this.context.newProgressbarForExtracting(size)) {
5✔
674
        for (Path root : fs.getRootDirectories()) {
11✔
675
          copy(root, targetDir, FileCopyMode.EXTRACT, (s, t, d) -> onFileCopiedFromZip(s, t, d, progressBar));
15✔
676
        }
1✔
677
      }
678
    } catch (IOException e) {
×
679
      throw new IllegalStateException("Failed to extract " + file + " to " + targetDir, e);
×
680
    }
1✔
681
  }
1✔
682

683
  @SuppressWarnings("unchecked")
684
  private void onFileCopiedFromZip(Path source, Path target, boolean directory, IdeProgressBar progressBar) {
685

686
    if (directory) {
2✔
687
      return;
1✔
688
    }
689
    if (!context.getSystemInfo().isWindows()) {
5✔
690
      try {
691
        Object attribute = Files.getAttribute(source, "zip:permissions");
6✔
692
        if (attribute instanceof Set<?> permissionSet) {
6✔
693
          Files.setPosixFilePermissions(target, (Set<PosixFilePermission>) permissionSet);
4✔
694
        }
695
      } catch (Exception e) {
×
696
        context.error(e, "Failed to transfer zip permissions for {}", target);
×
697
      }
1✔
698
    }
699
    progressBar.stepBy(getFileSize(target));
5✔
700
  }
1✔
701

702
  @Override
703
  public void extractTar(Path file, Path targetDir, TarCompression compression) {
704

705
    extractArchive(file, targetDir, in -> new TarArchiveInputStream(compression.unpack(in)));
13✔
706
  }
1✔
707

708
  @Override
709
  public void extractJar(Path file, Path targetDir) {
710

711
    extractZip(file, targetDir);
4✔
712
  }
1✔
713

714
  /**
715
   * @param permissions The integer as returned by {@link TarArchiveEntry#getMode()} that represents the file permissions of a file on a Unix file system.
716
   * @return A String representing the file permissions. E.g. "rwxrwxr-x" or "rw-rw-r--"
717
   */
718
  public static String generatePermissionString(int permissions) {
719

720
    // Ensure that only the last 9 bits are considered
721
    permissions &= 0b111111111;
4✔
722

723
    StringBuilder permissionStringBuilder = new StringBuilder("rwxrwxrwx");
5✔
724
    for (int i = 0; i < 9; i++) {
7✔
725
      int mask = 1 << i;
4✔
726
      char currentChar = ((permissions & mask) != 0) ? permissionStringBuilder.charAt(8 - i) : '-';
12✔
727
      permissionStringBuilder.setCharAt(8 - i, currentChar);
6✔
728
    }
729

730
    return permissionStringBuilder.toString();
3✔
731
  }
732

733
  private void extractArchive(
734
      Path file,
735
      Path targetDir,
736
      java.util.function.Function<InputStream, ArchiveInputStream<?>> unpacker
737
  ) {
738
    this.context.info("Extracting TAR file {} to {}", file, targetDir);
14✔
739

740
    final List<PathLink> links = new ArrayList<>();
4✔
741
    try (InputStream is = Files.newInputStream(file);
5✔
742
        ArchiveInputStream<?> ais = unpacker.apply(is);
5✔
743
        IdeProgressBar pb = this.context.newProgressbarForExtracting(getFileSize(file))) {
7✔
744

745
      final boolean isTar = (ais instanceof TarArchiveInputStream);
3✔
746
      final boolean isWindows = this.context.getSystemInfo().isWindows();
5✔
747
      final Path root = targetDir.toAbsolutePath().normalize();
4✔
748

749
      ArchiveEntry entry = ais.getNextEntry();
3✔
750
      while (entry != null) {
2✔
751
        String entryName = entry.getName();
3✔
752
        Path entryPath = resolveRelativePathSecure(entryName, root);
5✔
753
        PathPermissions permissions = null;
2✔
754
        PathLinkType linkType = null;
2✔
755
        if (entry instanceof TarArchiveEntry tae) {
6!
756
          if (tae.isSymbolicLink()) {
3!
757
            linkType = PathLinkType.SYMBOLIC_LINK;
×
758
          } else if (tae.isLink()) {
3!
759
            linkType = PathLinkType.HARD_LINK;
×
760
          }
761
          if (linkType == null) {
2!
762
            permissions = PathPermissions.of(tae.getMode());
5✔
763
          } else {
764
            Path parent = entryPath.getParent();
×
765
            String linkName = tae.getLinkName();
×
766
            Path linkTarget = parent.resolve(linkName).normalize();
×
767
            Path target = resolveRelativePathSecure(linkTarget, root, linkName);
×
768
            links.add(new PathLink(entryPath, target, linkType));
×
769
            mkdirs(parent);
×
770
          }
771
        }
772

773
        if (entry.isDirectory()) {
3✔
774
          mkdirs(entryPath);
4✔
775
        } else if (linkType == null) { // regular file
2!
776
          mkdirs(entryPath.getParent());
4✔
777
          Files.copy(ais, entryPath, StandardCopyOption.REPLACE_EXISTING);
10✔
778
          // POSIX perms on non-Windows
779
          if (!isWindows && (permissions != null)) {
4!
780
            setFilePermissions(entryPath, permissions, false);
5✔
781
          }
782
        }
783
        pb.stepBy(Math.max(0L, entry.getSize()));
6✔
784
        entry = ais.getNextEntry();
3✔
785
      }
1✔
786
      // post process links
787
      for (PathLink link : links) {
6!
788
        link(link);
×
789
      }
×
790
    } catch (Exception e) {
×
791
      throw new IllegalStateException("Failed to extract " + file + " to " + targetDir, e);
×
792
    }
1✔
793
  }
1✔
794

795
  private Path resolveRelativePathSecure(String entryName, Path root) {
796

797
    Path entryPath = root.resolve(entryName).normalize();
5✔
798
    return resolveRelativePathSecure(entryPath, root, entryName);
6✔
799
  }
800

801
  private Path resolveRelativePathSecure(Path entryPath, Path root, String entryName) {
802

803
    if (!entryPath.startsWith(root)) {
4!
804
      throw new IllegalStateException("Preventing path traversal attack from " + entryName + " to " + entryPath + " leaving " + root);
×
805
    }
806
    return entryPath;
2✔
807
  }
808

809

810
  @Override
811
  public void extractDmg(Path file, Path targetDir) {
812

813
    this.context.info("Extracting DMG file {} to {}", file, targetDir);
×
814
    assert this.context.getSystemInfo().isMac();
×
815

816
    Path mountPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES).resolve(IdeContext.FOLDER_VOLUME);
×
817
    mkdirs(mountPath);
×
818
    ProcessContext pc = this.context.newProcess();
×
819
    pc.executable("hdiutil");
×
820
    pc.addArgs("attach", "-quiet", "-nobrowse", "-mountpoint", mountPath, file);
×
821
    pc.run();
×
822
    Path appPath = findFirst(mountPath, p -> p.getFileName().toString().endsWith(".app"), false);
×
823
    if (appPath == null) {
×
824
      throw new IllegalStateException("Failed to unpack DMG as no MacOS *.app was found in file " + file);
×
825
    }
826

827
    copy(appPath, targetDir, FileCopyMode.COPY_TREE_OVERRIDE_TREE);
×
828
    pc.addArgs("detach", "-force", mountPath);
×
829
    pc.run();
×
830
  }
×
831

832
  @Override
833
  public void extractMsi(Path file, Path targetDir) {
834

835
    this.context.info("Extracting MSI file {} to {}", file, targetDir);
×
836
    this.context.newProcess().executable("msiexec").addArgs("/a", file, "/qn", "TARGETDIR=" + targetDir).run();
×
837
    // msiexec also creates a copy of the MSI
838
    Path msiCopy = targetDir.resolve(file.getFileName());
×
839
    delete(msiCopy);
×
840
  }
×
841

842
  @Override
843
  public void extractPkg(Path file, Path targetDir) {
844

845
    this.context.info("Extracting PKG file {} to {}", file, targetDir);
×
846
    Path tmpDirPkg = createTempDir("ide-pkg-");
×
847
    ProcessContext pc = this.context.newProcess();
×
848
    // we might also be able to use cpio from commons-compression instead of external xar...
849
    pc.executable("xar").addArgs("-C", tmpDirPkg, "-xf", file).run();
×
850
    Path contentPath = findFirst(tmpDirPkg, p -> p.getFileName().toString().equals("Payload"), true);
×
851
    extractTar(contentPath, targetDir, TarCompression.GZ);
×
852
    delete(tmpDirPkg);
×
853
  }
×
854

855
  @Override
856
  public void compress(Path dir, OutputStream out, String format) {
857

858
    String extension = FilenameUtil.getExtension(format);
3✔
859
    TarCompression tarCompression = TarCompression.of(extension);
3✔
860
    if (tarCompression != null) {
2!
861
      compressTar(dir, out, tarCompression);
6✔
862
    } else if (extension.equals("zip")) {
×
863
      compressZip(dir, out);
×
864
    } else {
865
      throw new IllegalArgumentException("Unsupported extension: " + extension);
×
866
    }
867
  }
1✔
868

869
  @Override
870
  public void compressTar(Path dir, OutputStream out, TarCompression tarCompression) {
871
    switch (tarCompression) {
8!
872
      case null -> compressTar(dir, out);
×
873
      case NONE -> compressTar(dir, out);
×
874
      case GZ -> compressTarGz(dir, out);
5✔
875
      case BZIP2 -> compressTarBzip2(dir, out);
×
876
      default -> throw new IllegalArgumentException("Unsupported tar compression: " + tarCompression);
×
877
    }
878
  }
1✔
879

880
  @Override
881
  public void compressTarGz(Path dir, OutputStream out) {
882
    try (GzipCompressorOutputStream gzOut = new GzipCompressorOutputStream(out)) {
5✔
883
      compressTarOrThrow(dir, gzOut);
4✔
884
    } catch (IOException e) {
×
885
      throw new IllegalStateException("Failed to compress directory " + dir + " to tar.gz file.", e);
×
886
    }
1✔
887
  }
1✔
888

889
  @Override
890
  public void compressTarBzip2(Path dir, OutputStream out) {
891
    try (BZip2CompressorOutputStream bzip2Out = new BZip2CompressorOutputStream(out)) {
×
892
      compressTarOrThrow(dir, bzip2Out);
×
893
    } catch (IOException e) {
×
894
      throw new IllegalStateException("Failed to compress directory " + dir + " to tar.bz2 file.", e);
×
895
    }
×
896
  }
×
897

898
  @Override
899
  public void compressTar(Path dir, OutputStream out) {
900
    try {
901
      compressTarOrThrow(dir, out);
×
902
    } catch (IOException e) {
×
903
      throw new IllegalStateException("Failed to compress directory " + dir + " to tar file.", e);
×
904
    }
×
905
  }
×
906

907
  private void compressTarOrThrow(Path dir, OutputStream out) throws IOException {
908
    try (TarArchiveOutputStream tarOut = new TarArchiveOutputStream(out)) {
5✔
909
      compressRecursive(dir, tarOut, "");
5✔
910
      tarOut.finish();
2✔
911
    }
912
  }
1✔
913

914
  @Override
915
  public void compressZip(Path dir, OutputStream out) {
916
    try (ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(out)) {
×
917
      compressRecursive(dir, zipOut, "");
×
918
      zipOut.finish();
×
919
    } catch (IOException e) {
×
920
      throw new IllegalStateException("Failed to compress directory " + dir + " to zip file.", e);
×
921
    }
×
922
  }
×
923

924
  private <E extends ArchiveEntry> void compressRecursive(Path path, ArchiveOutputStream<E> out, String relativePath) {
925
    try (Stream<Path> childStream = Files.list(path)) {
3✔
926
      Iterator<Path> iterator = childStream.iterator();
3✔
927
      while (iterator.hasNext()) {
3✔
928
        Path child = iterator.next();
4✔
929
        String relativeChildPath = relativePath + "/" + child.getFileName().toString();
6✔
930
        boolean isDirectory = Files.isDirectory(child);
5✔
931
        E archiveEntry = out.createArchiveEntry(child, relativeChildPath);
7✔
932
        if (archiveEntry instanceof TarArchiveEntry tarEntry) {
6!
933
          FileTime none = FileTime.fromMillis(0);
3✔
934
          tarEntry.setCreationTime(none);
3✔
935
          tarEntry.setModTime(none);
3✔
936
          tarEntry.setLastAccessTime(none);
3✔
937
          tarEntry.setLastModifiedTime(none);
3✔
938
          tarEntry.setUserId(0);
3✔
939
          tarEntry.setUserName("user");
3✔
940
          tarEntry.setGroupId(0);
3✔
941
          tarEntry.setGroupName("group");
3✔
942
          if (isDirectory) {
2✔
943
            tarEntry.setMode(MODE_RWX_RX_RX);
4✔
944
          } else {
945
            if (relativePath.endsWith("bin")) {
4✔
946
              tarEntry.setMode(MODE_RWX_RX_RX);
4✔
947
            } else {
948
              tarEntry.setMode(MODE_RW_R_R);
3✔
949
            }
950
          }
951
        }
952
        out.putArchiveEntry(archiveEntry);
3✔
953
        if (!isDirectory) {
2✔
954
          try (InputStream in = Files.newInputStream(child)) {
5✔
955
            IOUtils.copy(in, out);
4✔
956
          }
957
        }
958
        out.closeArchiveEntry();
2✔
959
        if (isDirectory) {
2✔
960
          compressRecursive(child, out, relativeChildPath);
5✔
961
        }
962
      }
1✔
963
    } catch (IOException e) {
×
964
      throw new IllegalStateException("Failed to compress " + path, e);
×
965
    }
1✔
966
  }
1✔
967

968
  @Override
969
  public void delete(Path path) {
970

971
    if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
9✔
972
      this.context.trace("Deleting {} skipped as the path does not exist.", path);
10✔
973
      return;
1✔
974
    }
975
    this.context.debug("Deleting {} ...", path);
10✔
976
    try {
977
      if (Files.isSymbolicLink(path) || isJunction(path)) {
7!
978
        Files.delete(path);
3✔
979
      } else {
980
        deleteRecursive(path);
3✔
981
      }
982
    } catch (IOException e) {
×
983
      throw new IllegalStateException("Failed to delete " + path, e);
×
984
    }
1✔
985
  }
1✔
986

987
  private void deleteRecursive(Path path) throws IOException {
988

989
    if (Files.isDirectory(path)) {
5✔
990
      try (Stream<Path> childStream = Files.list(path)) {
3✔
991
        Iterator<Path> iterator = childStream.iterator();
3✔
992
        while (iterator.hasNext()) {
3✔
993
          Path child = iterator.next();
4✔
994
          deleteRecursive(child);
3✔
995
        }
1✔
996
      }
997
    }
998
    this.context.trace("Deleting {} ...", path);
10✔
999
    boolean isSetWritable = setWritable(path, true);
5✔
1000
    if (!isSetWritable) {
2✔
1001
      this.context.debug("Couldn't give write access to file: " + path);
6✔
1002
    }
1003
    Files.delete(path);
2✔
1004
  }
1✔
1005

1006
  @Override
1007
  public Path findFirst(Path dir, Predicate<Path> filter, boolean recursive) {
1008

1009
    try {
1010
      if (!Files.isDirectory(dir)) {
5✔
1011
        return null;
2✔
1012
      }
1013
      return findFirstRecursive(dir, filter, recursive);
6✔
1014
    } catch (IOException e) {
×
1015
      throw new IllegalStateException("Failed to search for file in " + dir, e);
×
1016
    }
1017
  }
1018

1019
  private Path findFirstRecursive(Path dir, Predicate<Path> filter, boolean recursive) throws IOException {
1020

1021
    List<Path> folders = null;
2✔
1022
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
1023
      Iterator<Path> iterator = childStream.iterator();
3✔
1024
      while (iterator.hasNext()) {
3✔
1025
        Path child = iterator.next();
4✔
1026
        if (filter.test(child)) {
4✔
1027
          return child;
4✔
1028
        } else if (recursive && Files.isDirectory(child)) {
2!
1029
          if (folders == null) {
×
1030
            folders = new ArrayList<>();
×
1031
          }
1032
          folders.add(child);
×
1033
        }
1034
      }
1✔
1035
    }
4!
1036
    if (folders != null) {
2!
1037
      for (Path child : folders) {
×
1038
        Path match = findFirstRecursive(child, filter, recursive);
×
1039
        if (match != null) {
×
1040
          return match;
×
1041
        }
1042
      }
×
1043
    }
1044
    return null;
2✔
1045
  }
1046

1047
  @Override
1048
  public List<Path> listChildrenMapped(Path dir, Function<Path, Path> filter) {
1049

1050
    if (!Files.isDirectory(dir)) {
5✔
1051
      return List.of();
2✔
1052
    }
1053
    List<Path> children = new ArrayList<>();
4✔
1054
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
1055
      Iterator<Path> iterator = childStream.iterator();
3✔
1056
      while (iterator.hasNext()) {
3✔
1057
        Path child = iterator.next();
4✔
1058
        Path filteredChild = filter.apply(child);
5✔
1059
        if (filteredChild != null) {
2✔
1060
          if (filteredChild == child) {
3!
1061
            this.context.trace("Accepted file {}", child);
11✔
1062
          } else {
1063
            this.context.trace("Accepted file {} and mapped to {}", child, filteredChild);
×
1064
          }
1065
          children.add(filteredChild);
5✔
1066
        } else {
1067
          this.context.trace("Ignoring file {} according to filter", child);
10✔
1068
        }
1069
      }
1✔
1070
    } catch (IOException e) {
×
1071
      throw new IllegalStateException("Failed to find children of directory " + dir, e);
×
1072
    }
1✔
1073
    return children;
2✔
1074
  }
1075

1076
  @Override
1077
  public boolean isEmptyDir(Path dir) {
1078

1079
    return listChildren(dir, f -> true).isEmpty();
8✔
1080
  }
1081

1082
  private long getFileSize(Path file) {
1083

1084
    try {
1085
      return Files.size(file);
3✔
1086
    } catch (IOException e) {
×
1087
      this.context.warning(e.getMessage(), e);
×
1088
      return 0;
×
1089
    }
1090
  }
1091

1092
  private long getFileSizeRecursive(Path path) {
1093

1094
    long size = 0;
2✔
1095
    if (Files.isDirectory(path)) {
5✔
1096
      try (Stream<Path> childStream = Files.list(path)) {
3✔
1097
        Iterator<Path> iterator = childStream.iterator();
3✔
1098
        while (iterator.hasNext()) {
3✔
1099
          Path child = iterator.next();
4✔
1100
          size += getFileSizeRecursive(child);
6✔
1101
        }
1✔
1102
      } catch (IOException e) {
×
1103
        throw new RuntimeException("Failed to iterate children of folder " + path, e);
×
1104
      }
1✔
1105
    } else {
1106
      size += getFileSize(path);
6✔
1107
    }
1108
    return size;
2✔
1109
  }
1110

1111
  @Override
1112
  public Path findExistingFile(String fileName, List<Path> searchDirs) {
1113

1114
    for (Path dir : searchDirs) {
10!
1115
      Path filePath = dir.resolve(fileName);
4✔
1116
      try {
1117
        if (Files.exists(filePath)) {
5✔
1118
          return filePath;
2✔
1119
        }
1120
      } catch (Exception e) {
×
1121
        throw new IllegalStateException("Unexpected error while checking existence of file " + filePath + " .", e);
×
1122
      }
1✔
1123
    }
1✔
1124
    return null;
×
1125
  }
1126

1127
  @Override
1128
  public boolean setWritable(Path file, boolean writable) {
1129
    try {
1130
      // POSIX
1131
      PosixFileAttributeView posix = Files.getFileAttributeView(file, PosixFileAttributeView.class);
7✔
1132
      if (posix != null) {
2!
1133
        Set<PosixFilePermission> permissions = new HashSet<>(posix.readAttributes().permissions());
7✔
1134
        boolean changed;
1135
        if (writable) {
2!
1136
          changed = permissions.add(PosixFilePermission.OWNER_WRITE);
5✔
1137
        } else {
1138
          changed = permissions.remove(PosixFilePermission.OWNER_WRITE);
×
1139
        }
1140
        if (changed) {
2!
1141
          posix.setPermissions(permissions);
×
1142
        }
1143
        return true;
2✔
1144
      }
1145

1146
      // Windows
1147
      DosFileAttributeView dos = Files.getFileAttributeView(file, DosFileAttributeView.class);
×
1148
      if (dos != null) {
×
1149
        dos.setReadOnly(!writable);
×
1150
        return true;
×
1151
      }
1152

1153
      this.context.debug("Failed to set writing permission for file {}", file);
×
1154
      return false;
×
1155

1156
    } catch (IOException e) {
1✔
1157
      this.context.debug("Error occurred when trying to set writing permission for file " + file + ": " + e);
8✔
1158
      return false;
2✔
1159
    }
1160
  }
1161

1162
  @Override
1163
  public void makeExecutable(Path path, boolean confirm) {
1164

1165
    if (Files.exists(path)) {
5✔
1166
      if (skipPermissionsIfWindows(path)) {
4!
1167
        return;
×
1168
      }
1169
      Set<PosixFilePermission> existingPosixPermissions;
1170
      try {
1171
        // Read the current file permissions
1172
        existingPosixPermissions = Files.getPosixFilePermissions(path);
5✔
1173
      } catch (IOException e) {
×
1174
        throw new RuntimeException("Failed to get permissions for " + path, e);
×
1175
      }
1✔
1176

1177
      PathPermissions existingPermissions = PathPermissions.of(existingPosixPermissions);
3✔
1178
      PathPermissions executablePermissions = existingPermissions.makeExecutable();
3✔
1179
      boolean update = (executablePermissions != existingPermissions);
7✔
1180
      if (update) {
2✔
1181
        if (confirm) {
2!
1182
          boolean yesContinue = this.context.question(
×
1183
              "We want to execute {} but this command seems to lack executable permissions!\n"
1184
                  + "Most probably the tool vendor did forgot to add x-flags in the binary release package.\n"
1185
                  + "Before running the command, we suggest to set executable permissions to the file:\n"
1186
                  + "{}\n"
1187
                  + "For security reasons we ask for your confirmation so please check this request.\n"
1188
                  + "Changing permissions from {} to {}.\n"
1189
                  + "Do you confirm to make the command executable before running it?", path.getFileName(), path, existingPermissions, executablePermissions);
×
1190
          if (!yesContinue) {
×
1191
            return;
×
1192
          }
1193
        }
1194
        setFilePermissions(path, executablePermissions, false);
6✔
1195
      } else {
1196
        this.context.trace("Executable flags already present so no need to set them for file {}", path);
10✔
1197
      }
1198
    } else {
1✔
1199
      this.context.warning("Cannot set executable flag on file that does not exist: {}", path);
10✔
1200
    }
1201
  }
1✔
1202

1203
  @Override
1204
  public void setFilePermissions(Path path, PathPermissions permissions, boolean logErrorAndContinue) {
1205

1206
    if (skipPermissionsIfWindows(path)) {
4!
1207
      return;
×
1208
    }
1209
    try {
1210
      this.context.debug("Setting permissions for {} to {}", path, permissions);
14✔
1211
      // Set the new permissions
1212
      Files.setPosixFilePermissions(path, permissions.toPosix());
5✔
1213
    } catch (IOException e) {
×
1214
      if (logErrorAndContinue) {
×
1215
        this.context.warning().log(e, "Failed to set permissions to {} for path {}", permissions, path);
×
1216
      } else {
1217
        throw new RuntimeException("Failed to set permissions to " + permissions + " for path " + path, e);
×
1218
      }
1219
    }
1✔
1220
  }
1✔
1221

1222
  private boolean skipPermissionsIfWindows(Path path) {
1223
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1224
      this.context.trace("Windows does not have file permissions hence omitting for {}", path);
×
1225
      return true;
×
1226
    }
1227
    return false;
2✔
1228
  }
1229

1230
  @Override
1231
  public void touch(Path file) {
1232

1233
    if (Files.exists(file)) {
5✔
1234
      try {
1235
        Files.setLastModifiedTime(file, FileTime.fromMillis(System.currentTimeMillis()));
5✔
1236
      } catch (IOException e) {
×
1237
        throw new IllegalStateException("Could not update modification-time of " + file, e);
×
1238
      }
1✔
1239
    } else {
1240
      try {
1241
        Files.createFile(file);
5✔
1242
      } catch (IOException e) {
1✔
1243
        throw new IllegalStateException("Could not create empty file " + file, e);
8✔
1244
      }
1✔
1245
    }
1246
  }
1✔
1247

1248
  @Override
1249
  public String readFileContent(Path file) {
1250

1251
    this.context.trace("Reading content of file from {}", file);
10✔
1252
    if (!Files.exists((file))) {
5!
1253
      this.context.debug("File {} does not exist", file);
×
1254
      return null;
×
1255
    }
1256
    try {
1257
      String content = Files.readString(file);
3✔
1258
      this.context.trace("Completed reading {} character(s) from file {}", content.length(), file);
16✔
1259
      return content;
2✔
1260
    } catch (IOException e) {
×
1261
      throw new IllegalStateException("Failed to read file " + file, e);
×
1262
    }
1263
  }
1264

1265
  @Override
1266
  public void writeFileContent(String content, Path file, boolean createParentDir) {
1267

1268
    if (createParentDir) {
2✔
1269
      mkdirs(file.getParent());
4✔
1270
    }
1271
    if (content == null) {
2!
1272
      content = "";
×
1273
    }
1274
    this.context.trace("Writing content with {} character(s) to file {}", content.length(), file);
16✔
1275
    if (Files.exists(file)) {
5✔
1276
      this.context.info("Overriding content of file {}", file);
10✔
1277
    }
1278
    try {
1279
      Files.writeString(file, content);
6✔
1280
      this.context.trace("Wrote content to file {}", file);
10✔
1281
    } catch (IOException e) {
×
1282
      throw new RuntimeException("Failed to write file " + file, e);
×
1283
    }
1✔
1284
  }
1✔
1285

1286
  @Override
1287
  public List<String> readFileLines(Path file) {
1288

1289
    this.context.trace("Reading content of file from {}", file);
10✔
1290
    if (!Files.exists(file)) {
5✔
1291
      this.context.warning("File {} does not exist", file);
10✔
1292
      return null;
2✔
1293
    }
1294
    try {
1295
      List<String> content = Files.readAllLines(file);
3✔
1296
      this.context.trace("Completed reading {} lines from file {}", content.size(), file);
16✔
1297
      return content;
2✔
1298
    } catch (IOException e) {
×
1299
      throw new IllegalStateException("Failed to read file " + file, e);
×
1300
    }
1301
  }
1302

1303
  @Override
1304
  public void writeFileLines(List<String> content, Path file, boolean createParentDir) {
1305

1306
    if (createParentDir) {
2!
1307
      mkdirs(file.getParent());
×
1308
    }
1309
    if (content == null) {
2!
1310
      content = List.of();
×
1311
    }
1312
    this.context.trace("Writing content with {} lines to file {}", content.size(), file);
16✔
1313
    if (Files.exists(file)) {
5✔
1314
      this.context.debug("Overriding content of file {}", file);
10✔
1315
    }
1316
    try {
1317
      Files.write(file, content);
6✔
1318
      this.context.trace("Wrote content to file {}", file);
10✔
1319
    } catch (IOException e) {
×
1320
      throw new RuntimeException("Failed to write file " + file, e);
×
1321
    }
1✔
1322
  }
1✔
1323

1324
  @Override
1325
  public void readProperties(Path file, Properties properties) {
1326

1327
    try (Reader reader = Files.newBufferedReader(file)) {
3✔
1328
      properties.load(reader);
3✔
1329
      this.context.debug("Successfully loaded {} properties from {}", properties.size(), file);
16✔
1330
    } catch (IOException e) {
×
1331
      throw new IllegalStateException("Failed to read properties file: " + file, e);
×
1332
    }
1✔
1333
  }
1✔
1334

1335
  @Override
1336
  public void writeProperties(Properties properties, Path file, boolean createParentDir) {
1337

1338
    if (createParentDir) {
2✔
1339
      mkdirs(file.getParent());
4✔
1340
    }
1341
    try (Writer writer = Files.newBufferedWriter(file)) {
5✔
1342
      properties.store(writer, null); // do not get confused - Java still writes a date/time header that cannot be omitted
4✔
1343
      this.context.debug("Successfully saved {} properties to {}", properties.size(), file);
16✔
1344
    } catch (IOException e) {
×
1345
      throw new IllegalStateException("Failed to save properties file during tests.", e);
×
1346
    }
1✔
1347
  }
1✔
1348

1349
  @Override
1350
  public void readIniFile(Path file, IniFile iniFile) {
1351
    if (!Files.exists(file)) {
5!
1352
      this.context.debug("INI file {} does not exist.", iniFile);
×
1353
      return;
×
1354
    }
1355
    List<String> iniLines = readFileLines(file);
4✔
1356
    IniSection currentIniSection = iniFile.getInitialSection();
3✔
1357
    for (String line : iniLines) {
10✔
1358
      if (line.trim().startsWith("[")) {
5✔
1359
        currentIniSection = iniFile.getOrCreateSection(line);
5✔
1360
      } else if (line.isBlank() || IniComment.COMMENT_SYMBOLS.contains(line.trim().charAt(0))) {
11✔
1361
        currentIniSection.addComment(line);
4✔
1362
      } else {
1363
        int index = line.indexOf('=');
4✔
1364
        if (index > 0) {
2!
1365
          currentIniSection.setProperty(line);
3✔
1366
        }
1367
      }
1368
    }
1✔
1369
  }
1✔
1370

1371
  @Override
1372
  public void writeIniFile(IniFile iniFile, Path file, boolean createParentDir) {
1373
    String iniString = iniFile.toString();
3✔
1374
    writeFileContent(iniString, file, createParentDir);
5✔
1375
  }
1✔
1376

1377
  @Override
1378
  public Duration getFileAge(Path path) {
1379
    if (Files.exists(path)) {
5✔
1380
      try {
1381
        long currentTime = System.currentTimeMillis();
2✔
1382
        long fileModifiedTime = Files.getLastModifiedTime(path).toMillis();
6✔
1383
        return Duration.ofMillis(currentTime - fileModifiedTime);
5✔
1384
      } catch (IOException e) {
×
1385
        this.context.warning().log(e, "Could not get modification-time of {}.", path);
×
1386
      }
×
1387
    } else {
1388
      this.context.debug("Path {} is missing - skipping modification-time and file age check.", path);
10✔
1389
    }
1390
    return null;
2✔
1391
  }
1392

1393
  @Override
1394
  public boolean isFileAgeRecent(Path path, Duration cacheDuration) {
1395

1396
    Duration age = getFileAge(path);
4✔
1397
    if (age == null) {
2✔
1398
      return false;
2✔
1399
    }
1400
    context.debug("The path {} was last updated {} ago and caching duration is {}.", path, age, cacheDuration);
18✔
1401
    return (age.toMillis() <= cacheDuration.toMillis());
10✔
1402
  }
1403
}
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