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

devonfw / IDEasy / 22860792264

09 Mar 2026 03:25PM UTC coverage: 70.268% (-0.2%) from 70.481%
22860792264

push

github

web-flow
#404: #1713: advanced logging (#1714)

Co-authored-by: Kian <adasd>
Co-authored-by: KianRolf <kian.loroff@capgemini.com>
Co-authored-by: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com>
Co-authored-by: jan-vcapgemini <jan-vincent.hoelzle@capgemini.com>

4068 of 6386 branches covered (63.7%)

Branch coverage included in aggregate %.

10604 of 14494 relevant lines covered (73.16%)

3.08 hits per line

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

67.39
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.Version;
12
import java.net.http.HttpResponse;
13
import java.nio.file.FileSystem;
14
import java.nio.file.FileSystemException;
15
import java.nio.file.FileSystems;
16
import java.nio.file.Files;
17
import java.nio.file.LinkOption;
18
import java.nio.file.NoSuchFileException;
19
import java.nio.file.Path;
20
import java.nio.file.StandardCopyOption;
21
import java.nio.file.attribute.BasicFileAttributes;
22
import java.nio.file.attribute.DosFileAttributeView;
23
import java.nio.file.attribute.FileTime;
24
import java.nio.file.attribute.PosixFileAttributeView;
25
import java.nio.file.attribute.PosixFilePermission;
26
import java.security.DigestInputStream;
27
import java.security.MessageDigest;
28
import java.security.NoSuchAlgorithmException;
29
import java.time.Duration;
30
import java.time.LocalDateTime;
31
import java.util.ArrayList;
32
import java.util.HashSet;
33
import java.util.Iterator;
34
import java.util.List;
35
import java.util.Map;
36
import java.util.Properties;
37
import java.util.Set;
38
import java.util.function.Consumer;
39
import java.util.function.Function;
40
import java.util.function.Predicate;
41
import java.util.stream.Stream;
42

43
import org.apache.commons.compress.archivers.ArchiveEntry;
44
import org.apache.commons.compress.archivers.ArchiveInputStream;
45
import org.apache.commons.compress.archivers.ArchiveOutputStream;
46
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
47
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
48
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
49
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
50
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
51
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
52
import org.apache.commons.io.IOUtils;
53
import org.slf4j.Logger;
54
import org.slf4j.LoggerFactory;
55

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

70
/**
71
 * Implementation of {@link FileAccess}.
72
 */
73
public class FileAccessImpl extends HttpDownloader implements FileAccess {
74

75
  private static final Logger LOG = LoggerFactory.getLogger(FileAccessImpl.class);
3✔
76

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

79
  private static final String WINDOWS_FILE_LOCK_WARNING =
80
      "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"
81
          + WINDOWS_FILE_LOCK_DOCUMENTATION_PAGE;
82

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

85
  private final IdeContext context;
86

87
  /**
88
   * The constructor.
89
   *
90
   * @param context the {@link IdeContext} to use.
91
   */
92
  public FileAccessImpl(IdeContext context) {
93

94
    super();
2✔
95
    this.context = context;
3✔
96
  }
1✔
97

98
  @Override
99
  public void download(String url, Path target) {
100

101
    if (url.startsWith("http")) {
4✔
102
      downloadViaHttp(url, target);
5✔
103
    } else if (url.startsWith("ftp") || url.startsWith("sftp")) {
8!
104
      throw new IllegalArgumentException("Unsupported download URL: " + url);
×
105
    } else {
106
      Path source = Path.of(url);
5✔
107
      if (isFile(source)) {
4!
108
        // network drive
109
        copyFileWithProgressBar(source, target);
5✔
110
      } else {
111
        throw new IllegalArgumentException("Download path does not point to a downloadable file: " + url);
×
112
      }
113
    }
114
  }
1✔
115

116
  private void downloadViaHttp(String url, Path target) {
117

118
    List<Version> httpProtocols = IdeVariables.HTTP_VERSIONS.get(this.context);
6✔
119
    Exception lastException = null;
2✔
120
    if (httpProtocols.isEmpty()) {
3!
121
      try {
122
        downloadWithHttpVersion(url, target, null);
5✔
123
        return;
1✔
124
      } catch (Exception e) {
×
125
        lastException = e;
×
126
      }
×
127
    } else {
128
      for (Version version : httpProtocols) {
×
129
        try {
130
          downloadWithHttpVersion(url, target, version);
×
131
          return;
×
132
        } catch (Exception ex) {
×
133
          lastException = ex;
×
134
        }
135
      }
×
136
    }
137
    throw new IllegalStateException("Failed to download file from URL " + url + " to " + target, lastException);
×
138
  }
139

140
  private void downloadWithHttpVersion(String url, Path target, Version httpVersion) throws Exception {
141

142
    if (httpVersion == null) {
2!
143
      LOG.info("Trying to download {} from {}", target.getFileName(), url);
7✔
144
    } else {
145
      LOG.info("Trying to download {} from {} with HTTP protocol version {}", target.getFileName(), url, httpVersion);
×
146
    }
147
    mkdirs(target.getParent());
4✔
148
    this.context.getNetworkStatus().invokeNetworkTask(() ->
11✔
149
    {
150
      httpGet(url, httpVersion, (response) -> downloadFileWithProgressBar(url, target, response));
13✔
151
      return null;
2✔
152
    }, url);
153
  }
1✔
154

155
  /**
156
   * Downloads a file while showing a {@link IdeProgressBar}.
157
   *
158
   * @param url the url to download.
159
   * @param target Path of the target directory.
160
   * @param response the {@link HttpResponse} to use.
161
   */
162
  private void downloadFileWithProgressBar(String url, Path target, HttpResponse<InputStream> response) {
163

164
    long contentLength = response.headers().firstValueAsLong("content-length").orElse(-1);
7✔
165
    if (contentLength < 0) {
4✔
166
      LOG.warn("Content-Length was not provided by download from {}", url);
4✔
167
    }
168

169
    byte[] data = new byte[1024];
3✔
170
    boolean fileComplete = false;
2✔
171
    int count;
172

173
    try (InputStream body = response.body();
4✔
174
        FileOutputStream fileOutput = new FileOutputStream(target.toFile());
6✔
175
        BufferedOutputStream bufferedOut = new BufferedOutputStream(fileOutput, data.length);
7✔
176
        IdeProgressBar pb = this.context.newProgressBarForDownload(contentLength)) {
5✔
177
      while (!fileComplete) {
2✔
178
        count = body.read(data);
4✔
179
        if (count <= 0) {
2✔
180
          fileComplete = true;
3✔
181
        } else {
182
          bufferedOut.write(data, 0, count);
5✔
183
          pb.stepBy(count);
5✔
184
        }
185
      }
186
    } catch (Exception e) {
×
187
      throw new RuntimeException(e);
×
188
    }
1✔
189
  }
1✔
190

191
  private void copyFileWithProgressBar(Path source, Path target) {
192

193
    long size = getFileSize(source);
4✔
194
    if (size < 100_000) {
4✔
195
      copy(source, target, FileCopyMode.COPY_FILE_TO_TARGET_OVERRIDE);
5✔
196
      return;
1✔
197
    }
198
    try (InputStream in = Files.newInputStream(source); OutputStream out = Files.newOutputStream(target)) {
10✔
199
      byte[] buf = new byte[1024];
3✔
200
      try (IdeProgressBar pb = this.context.newProgressbarForCopying(size)) {
5✔
201
        int readBytes;
202
        while ((readBytes = in.read(buf)) > 0) {
6✔
203
          out.write(buf, 0, readBytes);
5✔
204
          pb.stepBy(readBytes);
5✔
205
        }
206
      } catch (Exception e) {
×
207
        throw new RuntimeException(e);
×
208
      }
1✔
209
    } catch (IOException e) {
×
210
      throw new RuntimeException("Failed to copy from " + source + " to " + target, e);
×
211
    }
1✔
212
  }
1✔
213

214
  @Override
215
  public String download(String url) {
216

217
    LOG.debug("Downloading text body from {}", url);
4✔
218
    return httpGetAsString(url);
3✔
219
  }
220

221
  @Override
222
  public void mkdirs(Path directory) {
223

224
    if (Files.isDirectory(directory)) {
5✔
225
      return;
1✔
226
    }
227
    LOG.trace("Creating directory {}", directory);
4✔
228
    try {
229
      Files.createDirectories(directory);
5✔
230
    } catch (IOException e) {
×
231
      throw new IllegalStateException("Failed to create directory " + directory, e);
×
232
    }
1✔
233
  }
1✔
234

235
  @Override
236
  public boolean isFile(Path file) {
237

238
    if (!Files.exists(file)) {
5!
239
      LOG.trace("File {} does not exist", file);
×
240
      return false;
×
241
    }
242
    if (Files.isDirectory(file)) {
5!
243
      LOG.trace("Path {} is a directory but a regular file was expected", file);
×
244
      return false;
×
245
    }
246
    return true;
2✔
247
  }
248

249
  @Override
250
  public boolean isExpectedFolder(Path folder) {
251

252
    if (Files.isDirectory(folder)) {
5✔
253
      return true;
2✔
254
    }
255
    LOG.warn("Expected folder was not found at {}", folder);
4✔
256
    return false;
2✔
257
  }
258

259
  @Override
260
  public String checksum(Path file, String hashAlgorithm) {
261

262
    MessageDigest md;
263
    try {
264
      md = MessageDigest.getInstance(hashAlgorithm);
×
265
    } catch (NoSuchAlgorithmException e) {
×
266
      throw new IllegalStateException("No such hash algorithm " + hashAlgorithm, e);
×
267
    }
×
268
    byte[] buffer = new byte[1024];
×
269
    try (InputStream is = Files.newInputStream(file); DigestInputStream dis = new DigestInputStream(is, md)) {
×
270
      int read = 0;
×
271
      while (read >= 0) {
×
272
        read = dis.read(buffer);
×
273
      }
274
    } catch (Exception e) {
×
275
      throw new IllegalStateException("Failed to read and hash file " + file, e);
×
276
    }
×
277
    byte[] digestBytes = md.digest();
×
278
    return HexUtil.toHexString(digestBytes);
×
279
  }
280

281
  @Override
282
  public boolean isJunction(Path path) {
283

284
    if (!SystemInfoImpl.INSTANCE.isWindows()) {
3!
285
      return false;
2✔
286
    }
287
    try {
288
      BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
×
289
      return attr.isOther() && attr.isDirectory();
×
290
    } catch (NoSuchFileException e) {
×
291
      return false; // file doesn't exist
×
292
    } catch (IOException e) {
×
293
      // For broken junctions, reading attributes might fail with various IOExceptions.
294
      // In such cases, we should check if the path exists without following links.
295
      // If it exists but we can't read its attributes, it's likely a broken junction.
296
      if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
×
297
        LOG.debug("Path {} exists but attributes cannot be read, likely a broken junction: {}", path, e.getMessage());
×
298
        return true; // Assume it's a broken junction
×
299
      }
300
      // If it doesn't exist at all, it's not a junction
301
      return false;
×
302
    }
303
  }
304

305
  @Override
306
  public Path backup(Path fileOrFolder) {
307

308
    if ((fileOrFolder != null) && (Files.isSymbolicLink(fileOrFolder) || isJunction(fileOrFolder))) {
9!
309
      delete(fileOrFolder);
4✔
310
    } else if ((fileOrFolder != null) && Files.exists(fileOrFolder)) {
7!
311
      LOG.trace("Going to backup {}", fileOrFolder);
4✔
312
      LocalDateTime now = LocalDateTime.now();
2✔
313
      String date = DateTimeUtil.formatDate(now, true);
4✔
314
      String time = DateTimeUtil.formatTime(now);
3✔
315
      String filename = fileOrFolder.getFileName().toString();
4✔
316
      Path backupBaseDir = this.context.getIdeHome();
4✔
317
      if (backupBaseDir == null) {
2!
318
        backupBaseDir = this.context.getIdePath();
×
319
      }
320
      Path backupPath = backupBaseDir.resolve(IdeContext.FOLDER_BACKUPS).resolve(date).resolve(time + "_" + filename);
10✔
321
      backupPath = appendParentPath(backupPath, fileOrFolder.getParent(), 2);
6✔
322
      mkdirs(backupPath);
3✔
323
      Path target = backupPath.resolve(filename);
4✔
324
      LOG.info("Creating backup by moving {} to {}", fileOrFolder, target);
5✔
325
      move(fileOrFolder, target);
6✔
326
      return target;
2✔
327
    } else {
328
      LOG.trace("Backup of {} skipped as the path does not exist.", fileOrFolder);
4✔
329
    }
330
    return fileOrFolder;
2✔
331
  }
332

333
  private static Path appendParentPath(Path path, Path parent, int max) {
334

335
    if ((parent == null) || (max <= 0)) {
4!
336
      return path;
2✔
337
    }
338
    return appendParentPath(path, parent.getParent(), max - 1).resolve(parent.getFileName());
11✔
339
  }
340

341
  @Override
342
  public void move(Path source, Path targetDir, StandardCopyOption... copyOptions) {
343

344
    LOG.trace("Moving {} to {}", source, targetDir);
5✔
345
    try {
346
      Files.move(source, targetDir, copyOptions);
5✔
347
    } catch (IOException e) {
×
348
      String fileType = Files.isSymbolicLink(source) ? "symlink" : isJunction(source) ? "junction" : Files.isDirectory(source) ? "directory" : "file";
×
349
      String message = "Failed to move " + fileType + ": " + source + " to " + targetDir + ".";
×
350
      if (this.context.getSystemInfo().isWindows()) {
×
351
        message = message + "\n" + WINDOWS_FILE_LOCK_WARNING;
×
352
      }
353
      throw new IllegalStateException(message, e);
×
354
    }
1✔
355
  }
1✔
356

357
  @Override
358
  public void copy(Path source, Path target, FileCopyMode mode, PathCopyListener listener) {
359

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

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

404
    if (Files.isDirectory(source)) {
5✔
405
      mkdirs(target);
3✔
406
      try (Stream<Path> childStream = Files.list(source)) {
3✔
407
        Iterator<Path> iterator = childStream.iterator();
3✔
408
        while (iterator.hasNext()) {
3✔
409
          Path child = iterator.next();
4✔
410
          copyRecursive(child, target.resolve(child.getFileName().toString()), mode, listener);
10✔
411
        }
1✔
412
      }
413
      listener.onCopy(source, target, true);
6✔
414
    } else if (Files.exists(source)) {
5!
415
      if (mode.isOverride()) {
3✔
416
        delete(target);
3✔
417
      }
418
      LOG.trace("Starting to {} {} to {}", mode.getOperation(), source, target);
18✔
419
      Files.copy(source, target);
6✔
420
      listener.onCopy(source, target, false);
6✔
421
    } else {
422
      throw new IOException("Path " + source + " does not exist.");
×
423
    }
424
  }
1✔
425

426
  /**
427
   * 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
428
   * {@link Path} that is neither a symbolic link nor a Windows junction.
429
   *
430
   * @param path the {@link Path} to delete.
431
   */
432
  private void deleteLinkIfExists(Path path) {
433

434
    boolean isJunction = isJunction(path); // since broken junctions are not detected by Files.exists()
4✔
435
    boolean isSymlink = Files.exists(path, LinkOption.NOFOLLOW_LINKS) && Files.isSymbolicLink(path);
16!
436

437
    assert !(isSymlink && isJunction);
5!
438

439
    if (isJunction || isSymlink) {
4!
440
      LOG.info("Deleting previous " + (isJunction ? "junction" : "symlink") + " at " + path);
8!
441
      try {
442
        Files.delete(path);
2✔
443
      } catch (IOException e) {
×
444
        throw new IllegalStateException("Failed to delete link at " + path, e);
×
445
      }
1✔
446
    }
447
  }
1✔
448

449
  /**
450
   * Adapts the given {@link Path} to be relative or absolute depending on the given {@code relative} flag. Additionally, {@link Path#toRealPath(LinkOption...)}
451
   * is applied to {@code target}.
452
   *
453
   * @param target the {@link Path} the link should point to and that is to be adapted.
454
   * @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}.
455
   * @param relative the {@code relative} flag.
456
   * @return the adapted {@link Path}.
457
   * @see #symlink(Path, Path, boolean)
458
   */
459
  private Path adaptPath(Path target, Path link, boolean relative) {
460

461
    if (!target.isAbsolute()) {
3✔
462
      target = link.resolveSibling(target);
4✔
463
    }
464
    try {
465
      target = target.toRealPath(LinkOption.NOFOLLOW_LINKS); // to transform ../d1/../d2 to ../d2
9✔
466
    } catch (IOException e) {
×
467
      throw new RuntimeException("Failed to get real path of " + target, e);
×
468
    }
1✔
469
    if (relative) {
2✔
470
      target = link.getParent().relativize(target);
5✔
471
      // to make relative links like this work: dir/link -> dir
472
      target = (target.toString().isEmpty()) ? Path.of(".") : target;
11✔
473
    }
474
    return target;
2✔
475
  }
476

477
  /**
478
   * Creates a Windows link using mklink at {@code link} pointing to {@code target}.
479
   *
480
   * @param target the {@link Path} the link will point to.
481
   * @param link the {@link Path} where to create the link.
482
   * @param type the {@link PathLinkType}.
483
   */
484
  private void mklinkOnWindows(Path target, Path link, PathLinkType type) {
485

486
    LOG.trace("Creating a Windows {} at {} pointing to {}", type, link, target);
×
487
    ProcessContext pc = this.context.newProcess().executable("cmd").addArgs("/c", "mklink", type.getMklinkOption());
×
488
    Path parent = link.getParent();
×
489
    if (parent == null) {
×
490
      parent = Path.of(".");
×
491
    }
492
    Path absolute = parent.resolve(target).toAbsolutePath().normalize();
×
493
    if (type == PathLinkType.SYMBOLIC_LINK && Files.isDirectory(absolute)) {
×
494
      pc = pc.addArg("/j");
×
495
      target = absolute;
×
496
    }
497
    pc = pc.addArgs(link.toString(), target.toString());
×
498
    ProcessResult result = pc.run(ProcessMode.DEFAULT);
×
499
    result.failOnError();
×
500
  }
×
501

502
  @Override
503
  public void link(Path source, Path link, boolean relative, PathLinkType type) {
504

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

537
  @Override
538
  public Path toRealPath(Path path) {
539

540
    return toRealPath(path, true);
5✔
541
  }
542

543
  @Override
544
  public Path toCanonicalPath(Path path) {
545

546
    return toRealPath(path, false);
5✔
547
  }
548

549
  private Path toRealPath(Path path, boolean resolveLinks) {
550

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

567
  @Override
568
  public Path createTempDir(String name) {
569

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

588
  @Override
589
  public void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtractHook, boolean extract) {
590

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

629
  private void postExtractHook(Consumer<Path> postExtractHook, Path properInstallDir) {
630

631
    if (postExtractHook != null) {
2✔
632
      postExtractHook.accept(properInstallDir);
3✔
633
    }
634
  }
1✔
635

636
  /**
637
   * @param path the {@link Path} to start the recursive search from.
638
   * @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
639
   *     item in their respective directory and {@code s} is not named "bin".
640
   */
641
  private Path getProperInstallationSubDirOf(Path path, Path archiveFile) {
642

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

660
  @Override
661
  public void extractZip(Path file, Path targetDir) {
662

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

680
  @SuppressWarnings("unchecked")
681
  private void onFileCopiedFromZip(Path source, Path target, boolean directory, IdeProgressBar progressBar) {
682

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

699
  @Override
700
  public void extractTar(Path file, Path targetDir, TarCompression compression) {
701

702
    extractArchive(file, targetDir, in -> new TarArchiveInputStream(compression.unpack(in)));
13✔
703
  }
1✔
704

705
  @Override
706
  public void extractJar(Path file, Path targetDir) {
707

708
    extractZip(file, targetDir);
4✔
709
  }
1✔
710

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

717
    // Ensure that only the last 9 bits are considered
718
    permissions &= 0b111111111;
4✔
719

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

727
    return permissionStringBuilder.toString();
3✔
728
  }
729

730
  private void extractArchive(Path file, Path targetDir, Function<InputStream, ArchiveInputStream<?>> unpacker) {
731

732
    LOG.info("Extracting TAR file {} to {}", file, targetDir);
5✔
733

734
    final List<PathLink> links = new ArrayList<>();
4✔
735
    try (InputStream is = Files.newInputStream(file);
5✔
736
        ArchiveInputStream<?> ais = unpacker.apply(is);
5✔
737
        IdeProgressBar pb = this.context.newProgressbarForExtracting(getFileSize(file))) {
7✔
738

739
      final Path root = targetDir.toAbsolutePath().normalize();
4✔
740

741
      ArchiveEntry entry = ais.getNextEntry();
3✔
742
      while (entry != null) {
2✔
743
        String entryName = entry.getName();
3✔
744
        Path entryPath = resolveRelativePathSecure(entryName, root);
5✔
745
        PathPermissions permissions = null;
2✔
746
        PathLinkType linkType = null;
2✔
747
        if (entry instanceof TarArchiveEntry tae) {
6!
748
          if (tae.isSymbolicLink()) {
3✔
749
            linkType = PathLinkType.SYMBOLIC_LINK;
3✔
750
          } else if (tae.isLink()) {
3!
751
            linkType = PathLinkType.HARD_LINK;
×
752
          }
753
          if (linkType == null) {
2✔
754
            permissions = PathPermissions.of(tae.getMode());
5✔
755
          } else {
756
            Path parent = entryPath.getParent();
3✔
757
            String sourcePathString = tae.getLinkName();
3✔
758
            Path source = parent.resolve(sourcePathString).normalize();
5✔
759
            source = resolveRelativePathSecure(source, root, sourcePathString);
6✔
760
            links.add(new PathLink(source, entryPath, linkType));
9✔
761
            mkdirs(parent);
3✔
762
          }
763
        }
764
        if (entry.isDirectory()) {
3✔
765
          mkdirs(entryPath);
4✔
766
        } else if (linkType == null) { // regular file
2✔
767
          mkdirs(entryPath.getParent());
4✔
768
          Files.copy(ais, entryPath, StandardCopyOption.REPLACE_EXISTING);
10✔
769
          // POSIX perms on non-Windows
770
          if (permissions != null) {
2!
771
            setFilePermissions(entryPath, permissions, false);
5✔
772
          }
773
        }
774
        pb.stepBy(Math.max(0L, entry.getSize()));
6✔
775
        entry = ais.getNextEntry();
3✔
776
      }
1✔
777
      // post process links
778
      for (PathLink link : links) {
10✔
779
        link(link);
3✔
780
      }
1✔
781
    } catch (Exception e) {
×
782
      throw new IllegalStateException("Failed to extract " + file + " to " + targetDir, e);
×
783
    }
1✔
784
  }
1✔
785

786
  private Path resolveRelativePathSecure(String entryName, Path root) {
787

788
    Path entryPath = root.resolve(entryName).normalize();
5✔
789
    return resolveRelativePathSecure(entryPath, root, entryName);
6✔
790
  }
791

792
  private Path resolveRelativePathSecure(Path entryPath, Path root, String entryName) {
793

794
    if (!entryPath.startsWith(root)) {
4!
795
      throw new IllegalStateException("Preventing path traversal attack from " + entryName + " to " + entryPath + " leaving " + root);
×
796
    }
797
    return entryPath;
2✔
798
  }
799

800
  @Override
801
  public void extractDmg(Path file, Path targetDir) {
802

803
    LOG.info("Extracting DMG file {} to {}", file, targetDir);
×
804
    assert this.context.getSystemInfo().isMac();
×
805

806
    Path mountPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES).resolve(IdeContext.FOLDER_VOLUME);
×
807
    mkdirs(mountPath);
×
808
    ProcessContext pc = this.context.newProcess();
×
809
    pc.executable("hdiutil");
×
810
    pc.addArgs("attach", "-quiet", "-nobrowse", "-mountpoint", mountPath, file);
×
811
    pc.run();
×
812
    Path appPath = findFirst(mountPath, p -> p.getFileName().toString().endsWith(".app"), false);
×
813
    if (appPath == null) {
×
814
      throw new IllegalStateException("Failed to unpack DMG as no MacOS *.app was found in file " + file);
×
815
    }
816

817
    copy(appPath, targetDir, FileCopyMode.COPY_TREE_OVERRIDE_TREE);
×
818
    pc.addArgs("detach", "-force", mountPath);
×
819
    pc.run();
×
820
  }
×
821

822
  @Override
823
  public void extractMsi(Path file, Path targetDir) {
824

825
    LOG.info("Extracting MSI file {} to {}", file, targetDir);
×
826
    this.context.newProcess().executable("msiexec").addArgs("/a", file, "/qn", "TARGETDIR=" + targetDir).run();
×
827
    // msiexec also creates a copy of the MSI
828
    Path msiCopy = targetDir.resolve(file.getFileName());
×
829
    delete(msiCopy);
×
830
  }
×
831

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

835
    LOG.info("Extracting PKG file {} to {}", file, targetDir);
×
836
    Path tmpDirPkg = createTempDir("ide-pkg-");
×
837
    ProcessContext pc = this.context.newProcess();
×
838
    // we might also be able to use cpio from commons-compression instead of external xar...
839
    pc.executable("xar").addArgs("-C", tmpDirPkg, "-xf", file).run();
×
840
    Path contentPath = findFirst(tmpDirPkg, p -> p.getFileName().toString().equals("Payload"), true);
×
841
    extractTar(contentPath, targetDir, TarCompression.GZ);
×
842
    delete(tmpDirPkg);
×
843
  }
×
844

845
  @Override
846
  public void compress(Path dir, OutputStream out, String path) {
847

848
    String extension = FilenameUtil.getExtension(path);
3✔
849
    TarCompression tarCompression = TarCompression.of(extension);
3✔
850
    if (tarCompression != null) {
2!
851
      compressTar(dir, out, tarCompression);
6✔
852
    } else if (extension.equals("zip")) {
×
853
      compressZip(dir, out);
×
854
    } else {
855
      throw new IllegalArgumentException("Unsupported extension: " + extension);
×
856
    }
857
  }
1✔
858

859
  @Override
860
  public void compressTar(Path dir, OutputStream out, TarCompression tarCompression) {
861

862
    switch (tarCompression) {
8!
863
      case null -> compressTar(dir, out);
×
864
      case NONE -> compressTar(dir, out);
×
865
      case GZ -> compressTarGz(dir, out);
5✔
866
      case BZIP2 -> compressTarBzip2(dir, out);
×
867
      default -> throw new IllegalArgumentException("Unsupported tar compression: " + tarCompression);
×
868
    }
869
  }
1✔
870

871
  @Override
872
  public void compressTarGz(Path dir, OutputStream out) {
873

874
    try (GzipCompressorOutputStream gzOut = new GzipCompressorOutputStream(out)) {
5✔
875
      compressTarOrThrow(dir, gzOut);
4✔
876
    } catch (IOException e) {
×
877
      throw new IllegalStateException("Failed to compress directory " + dir + " to tar.gz file.", e);
×
878
    }
1✔
879
  }
1✔
880

881
  @Override
882
  public void compressTarBzip2(Path dir, OutputStream out) {
883

884
    try (BZip2CompressorOutputStream bzip2Out = new BZip2CompressorOutputStream(out)) {
×
885
      compressTarOrThrow(dir, bzip2Out);
×
886
    } catch (IOException e) {
×
887
      throw new IllegalStateException("Failed to compress directory " + dir + " to tar.bz2 file.", e);
×
888
    }
×
889
  }
×
890

891
  @Override
892
  public void compressTar(Path dir, OutputStream out) {
893

894
    try {
895
      compressTarOrThrow(dir, out);
×
896
    } catch (IOException e) {
×
897
      throw new IllegalStateException("Failed to compress directory " + dir + " to tar file.", e);
×
898
    }
×
899
  }
×
900

901
  private void compressTarOrThrow(Path dir, OutputStream out) throws IOException {
902

903
    try (TarArchiveOutputStream tarOut = new TarArchiveOutputStream(out)) {
5✔
904
      compressRecursive(dir, tarOut, "");
5✔
905
      tarOut.finish();
2✔
906
    }
907
  }
1✔
908

909
  @Override
910
  public void compressZip(Path dir, OutputStream out) {
911

912
    try (ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(out)) {
×
913
      compressRecursive(dir, zipOut, "");
×
914
      zipOut.finish();
×
915
    } catch (IOException e) {
×
916
      throw new IllegalStateException("Failed to compress directory " + dir + " to zip file.", e);
×
917
    }
×
918
  }
×
919

920
  private <E extends ArchiveEntry> void compressRecursive(Path path, ArchiveOutputStream<E> out, String relativePath) {
921

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

958
  @Override
959
  public void delete(Path path) {
960

961
    if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
9✔
962
      LOG.trace("Deleting {} skipped as the path does not exist.", path);
4✔
963
      return;
1✔
964
    }
965
    LOG.debug("Deleting {} ...", path);
4✔
966
    try {
967
      if (Files.isSymbolicLink(path) || isJunction(path)) {
7!
968
        Files.delete(path);
3✔
969
      } else {
970
        deleteRecursive(path);
3✔
971
      }
972
    } catch (IOException e) {
×
973
      throw new IllegalStateException("Failed to delete " + path, e);
×
974
    }
1✔
975
  }
1✔
976

977
  private void deleteRecursive(Path path) throws IOException {
978

979
    if (Files.isDirectory(path)) {
5✔
980
      try (Stream<Path> childStream = Files.list(path)) {
3✔
981
        Iterator<Path> iterator = childStream.iterator();
3✔
982
        while (iterator.hasNext()) {
3✔
983
          Path child = iterator.next();
4✔
984
          deleteRecursive(child);
3✔
985
        }
1✔
986
      }
987
    }
988
    LOG.trace("Deleting {} ...", path);
4✔
989
    boolean isSetWritable = setWritable(path, true);
5✔
990
    if (!isSetWritable) {
2✔
991
      LOG.debug("Couldn't give write access to file: {}", path);
4✔
992
    }
993
    Files.delete(path);
2✔
994
  }
1✔
995

996
  @Override
997
  public Path findFirst(Path dir, Predicate<Path> filter, boolean recursive) {
998

999
    try {
1000
      if (!Files.isDirectory(dir)) {
5!
1001
        return null;
×
1002
      }
1003
      return findFirstRecursive(dir, filter, recursive);
6✔
1004
    } catch (IOException e) {
×
1005
      throw new IllegalStateException("Failed to search for file in " + dir, e);
×
1006
    }
1007
  }
1008

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

1011
    List<Path> folders = null;
2✔
1012
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
1013
      Iterator<Path> iterator = childStream.iterator();
3✔
1014
      while (iterator.hasNext()) {
3✔
1015
        Path child = iterator.next();
4✔
1016
        if (filter.test(child)) {
4✔
1017
          return child;
4✔
1018
        } else if (recursive && Files.isDirectory(child)) {
2!
1019
          if (folders == null) {
×
1020
            folders = new ArrayList<>();
×
1021
          }
1022
          folders.add(child);
×
1023
        }
1024
      }
1✔
1025
    }
4!
1026
    if (folders != null) {
2!
1027
      for (Path child : folders) {
×
1028
        Path match = findFirstRecursive(child, filter, recursive);
×
1029
        if (match != null) {
×
1030
          return match;
×
1031
        }
1032
      }
×
1033
    }
1034
    return null;
2✔
1035
  }
1036

1037
  @Override
1038
  public Path findAncestor(Path path, Path baseDir, int subfolderCount) {
1039

1040
    if ((path == null) || (baseDir == null)) {
4!
1041
      LOG.debug("Path should not be null for findAncestor.");
3✔
1042
      return null;
2✔
1043
    }
1044
    if (subfolderCount <= 0) {
2!
1045
      throw new IllegalArgumentException("Subfolder count: " + subfolderCount);
×
1046
    }
1047
    // 1. option relativize
1048
    // 2. recursive getParent
1049
    // 3. loop getParent???
1050
    // 4. getName + getNameCount
1051
    path = path.toAbsolutePath().normalize();
4✔
1052
    baseDir = baseDir.toAbsolutePath().normalize();
4✔
1053
    int directoryNameCount = path.getNameCount();
3✔
1054
    int baseDirNameCount = baseDir.getNameCount();
3✔
1055
    int delta = directoryNameCount - baseDirNameCount - subfolderCount;
6✔
1056
    if (delta < 0) {
2!
1057
      return null;
×
1058
    }
1059
    // ensure directory is a sub-folder of baseDir
1060
    for (int i = 0; i < baseDirNameCount; i++) {
7✔
1061
      if (!path.getName(i).toString().equals(baseDir.getName(i).toString())) {
10✔
1062
        return null;
2✔
1063
      }
1064
    }
1065
    Path result = path;
2✔
1066
    while (delta > 0) {
2✔
1067
      result = result.getParent();
3✔
1068
      delta--;
2✔
1069
    }
1070
    return result;
2✔
1071
  }
1072

1073
  @Override
1074
  public List<Path> listChildrenMapped(Path dir, Function<Path, Path> filter) {
1075

1076
    if (!Files.isDirectory(dir)) {
5✔
1077
      return List.of();
2✔
1078
    }
1079
    List<Path> children = new ArrayList<>();
4✔
1080
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
1081
      Iterator<Path> iterator = childStream.iterator();
3✔
1082
      while (iterator.hasNext()) {
3✔
1083
        Path child = iterator.next();
4✔
1084
        Path filteredChild = filter.apply(child);
5✔
1085
        if (filteredChild != null) {
2✔
1086
          if (filteredChild == child) {
3!
1087
            LOG.trace("Accepted file {}", child);
5✔
1088
          } else {
1089
            LOG.trace("Accepted file {} and mapped to {}", child, filteredChild);
×
1090
          }
1091
          children.add(filteredChild);
5✔
1092
        } else {
1093
          LOG.trace("Ignoring file {} according to filter", child);
4✔
1094
        }
1095
      }
1✔
1096
    } catch (IOException e) {
×
1097
      throw new IllegalStateException("Failed to find children of directory " + dir, e);
×
1098
    }
1✔
1099
    return children;
2✔
1100
  }
1101

1102
  @Override
1103
  public boolean isEmptyDir(Path dir) {
1104

1105
    return listChildren(dir, f -> true).isEmpty();
8✔
1106
  }
1107

1108
  @Override
1109
  public boolean isNonEmptyFile(Path file) {
1110

1111
    if (Files.isRegularFile(file)) {
5✔
1112
      return (getFileSize(file) > 0);
10✔
1113
    }
1114
    return false;
2✔
1115
  }
1116

1117
  private long getFileSize(Path file) {
1118

1119
    try {
1120
      return Files.size(file);
3✔
1121
    } catch (IOException e) {
×
1122
      LOG.warn("Failed to determine size of file {}: {}", file, e.toString(), e);
×
1123
      return 0;
×
1124
    }
1125
  }
1126

1127
  private long getFileSizeRecursive(Path path) {
1128

1129
    long size = 0;
2✔
1130
    if (Files.isDirectory(path)) {
5✔
1131
      try (Stream<Path> childStream = Files.list(path)) {
3✔
1132
        Iterator<Path> iterator = childStream.iterator();
3✔
1133
        while (iterator.hasNext()) {
3✔
1134
          Path child = iterator.next();
4✔
1135
          size += getFileSizeRecursive(child);
6✔
1136
        }
1✔
1137
      } catch (IOException e) {
×
1138
        throw new RuntimeException("Failed to iterate children of folder " + path, e);
×
1139
      }
1✔
1140
    } else {
1141
      size += getFileSize(path);
6✔
1142
    }
1143
    return size;
2✔
1144
  }
1145

1146
  @Override
1147
  public Path findExistingFile(String fileName, List<Path> searchDirs) {
1148

1149
    for (Path dir : searchDirs) {
10!
1150
      Path filePath = dir.resolve(fileName);
4✔
1151
      try {
1152
        if (Files.exists(filePath)) {
5✔
1153
          return filePath;
2✔
1154
        }
1155
      } catch (Exception e) {
×
1156
        throw new IllegalStateException("Unexpected error while checking existence of file " + filePath + " .", e);
×
1157
      }
1✔
1158
    }
1✔
1159
    return null;
×
1160
  }
1161

1162
  @Override
1163
  public boolean setWritable(Path file, boolean writable) {
1164

1165
    try {
1166
      // POSIX
1167
      PosixFileAttributeView posix = Files.getFileAttributeView(file, PosixFileAttributeView.class);
7✔
1168
      if (posix != null) {
2!
1169
        Set<PosixFilePermission> permissions = new HashSet<>(posix.readAttributes().permissions());
7✔
1170
        boolean changed;
1171
        if (writable) {
2!
1172
          changed = permissions.add(PosixFilePermission.OWNER_WRITE);
5✔
1173
        } else {
1174
          changed = permissions.remove(PosixFilePermission.OWNER_WRITE);
×
1175
        }
1176
        if (changed) {
2!
1177
          posix.setPermissions(permissions);
×
1178
        }
1179
        return true;
2✔
1180
      }
1181

1182
      // Windows
1183
      DosFileAttributeView dos = Files.getFileAttributeView(file, DosFileAttributeView.class);
×
1184
      if (dos != null) {
×
1185
        dos.setReadOnly(!writable);
×
1186
        return true;
×
1187
      }
1188

1189
      LOG.debug("Failed to set writing permission for file {}", file);
×
1190
      return false;
×
1191

1192
    } catch (IOException e) {
1✔
1193
      LOG.debug("Error occurred when trying to set writing permission for file {}: {}", file, e.toString(), e);
18✔
1194
      return false;
2✔
1195
    }
1196
  }
1197

1198
  @Override
1199
  public void makeExecutable(Path path, boolean confirm) {
1200

1201
    if (Files.exists(path)) {
5✔
1202
      if (skipPermissionsIfWindows(path)) {
4!
1203
        return;
×
1204
      }
1205
      PathPermissions existingPermissions = getFilePermissions(path);
4✔
1206
      PathPermissions executablePermissions = existingPermissions.makeExecutable();
3✔
1207
      boolean update = (executablePermissions != existingPermissions);
7✔
1208
      if (update) {
2✔
1209
        if (confirm) {
2!
1210
          boolean yesContinue = this.context.question(
×
1211
              "We want to execute {} but this command seems to lack executable permissions!\n"
1212
                  + "Most probably the tool vendor did forget to add x-flags in the binary release package.\n"
1213
                  + "Before running the command, we suggest to set executable permissions to the file:\n"
1214
                  + "{}\n"
1215
                  + "For security reasons we ask for your confirmation so please check this request.\n"
1216
                  + "Changing permissions from {} to {}.\n"
1217
                  + "Do you confirm to make the command executable before running it?", path.getFileName(), path, existingPermissions, executablePermissions);
×
1218
          if (!yesContinue) {
×
1219
            return;
×
1220
          }
1221
        }
1222
        setFilePermissions(path, executablePermissions, false);
6✔
1223
      } else {
1224
        LOG.trace("Executable flags already present so no need to set them for file {}", path);
4✔
1225
      }
1226
    } else {
1✔
1227
      LOG.warn("Cannot set executable flag on file that does not exist: {}", path);
4✔
1228
    }
1229
  }
1✔
1230

1231
  @Override
1232
  public PathPermissions getFilePermissions(Path path) {
1233

1234
    PathPermissions pathPermissions;
1235
    String info = "";
2✔
1236
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1237
      info = "mocked-";
×
1238
      if (Files.isDirectory(path)) {
×
1239
        pathPermissions = PathPermissions.MODE_RWX_RX_RX;
×
1240
      } else {
1241
        Path parent = path.getParent();
×
1242
        if ((parent != null) && (parent.getFileName().toString().equals("bin"))) {
×
1243
          pathPermissions = PathPermissions.MODE_RWX_RX_RX;
×
1244
        } else {
1245
          pathPermissions = PathPermissions.MODE_RW_R_R;
×
1246
        }
1247
      }
×
1248
    } else {
1249
      Set<PosixFilePermission> permissions;
1250
      try {
1251
        // Read the current file permissions
1252
        permissions = Files.getPosixFilePermissions(path);
5✔
1253
      } catch (IOException e) {
×
1254
        throw new RuntimeException("Failed to get permissions for " + path, e);
×
1255
      }
1✔
1256
      pathPermissions = PathPermissions.of(permissions);
3✔
1257
    }
1258
    LOG.trace("Read {}permissions of {} as {}.", info, path, pathPermissions);
17✔
1259
    return pathPermissions;
2✔
1260
  }
1261

1262
  @Override
1263
  public void setFilePermissions(Path path, PathPermissions permissions, boolean logErrorAndContinue) {
1264

1265
    if (skipPermissionsIfWindows(path)) {
4!
1266
      return;
×
1267
    }
1268
    try {
1269
      LOG.debug("Setting permissions for {} to {}", path, permissions);
5✔
1270
      // Set the new permissions
1271
      Files.setPosixFilePermissions(path, permissions.toPosix());
5✔
1272
    } catch (IOException e) {
×
1273
      String message = "Failed to set permissions to " + permissions + " for path " + path;
×
1274
      if (logErrorAndContinue) {
×
1275
        LOG.warn(message, e);
×
1276
      } else {
1277
        throw new RuntimeException(message, e);
×
1278
      }
1279
    }
1✔
1280
  }
1✔
1281

1282
  private boolean skipPermissionsIfWindows(Path path) {
1283

1284
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1285
      LOG.trace("Windows does not have file permissions hence omitting for {}", path);
×
1286
      return true;
×
1287
    }
1288
    return false;
2✔
1289
  }
1290

1291
  @Override
1292
  public void touch(Path file) {
1293

1294
    if (Files.exists(file)) {
5✔
1295
      try {
1296
        Files.setLastModifiedTime(file, FileTime.fromMillis(System.currentTimeMillis()));
5✔
1297
      } catch (IOException e) {
×
1298
        throw new IllegalStateException("Could not update modification-time of " + file, e);
×
1299
      }
1✔
1300
    } else {
1301
      try {
1302
        Files.createFile(file);
5✔
1303
      } catch (IOException e) {
1✔
1304
        throw new IllegalStateException("Could not create empty file " + file, e);
8✔
1305
      }
1✔
1306
    }
1307
  }
1✔
1308

1309
  @Override
1310
  public String readFileContent(Path file) {
1311

1312
    LOG.trace("Reading content of file from {}", file);
4✔
1313
    if (!Files.exists((file))) {
5!
1314
      LOG.debug("File {} does not exist", file);
×
1315
      return null;
×
1316
    }
1317
    try {
1318
      String content = Files.readString(file);
3✔
1319
      LOG.trace("Completed reading {} character(s) from file {}", content.length(), file);
7✔
1320
      return content;
2✔
1321
    } catch (IOException e) {
×
1322
      throw new IllegalStateException("Failed to read file " + file, e);
×
1323
    }
1324
  }
1325

1326
  @Override
1327
  public void writeFileContent(String content, Path file, boolean createParentDir) {
1328

1329
    if (createParentDir) {
2✔
1330
      mkdirs(file.getParent());
4✔
1331
    }
1332
    if (content == null) {
2!
1333
      content = "";
×
1334
    }
1335
    LOG.trace("Writing content with {} character(s) to file {}", content.length(), file);
7✔
1336
    if (Files.exists(file)) {
5✔
1337
      LOG.info("Overriding content of file {}", file);
4✔
1338
    }
1339
    try {
1340
      Files.writeString(file, content);
6✔
1341
      LOG.trace("Wrote content to file {}", file);
4✔
1342
    } catch (IOException e) {
×
1343
      throw new RuntimeException("Failed to write file " + file, e);
×
1344
    }
1✔
1345
  }
1✔
1346

1347
  @Override
1348
  public List<String> readFileLines(Path file) {
1349

1350
    LOG.trace("Reading lines of file from {}", file);
4✔
1351
    if (!Files.exists(file)) {
5✔
1352
      LOG.warn("File {} does not exist", file);
4✔
1353
      return null;
2✔
1354
    }
1355
    try {
1356
      List<String> content = Files.readAllLines(file);
3✔
1357
      LOG.trace("Completed reading {} lines from file {}", content.size(), file);
7✔
1358
      return content;
2✔
1359
    } catch (IOException e) {
×
1360
      throw new IllegalStateException("Failed to read file " + file, e);
×
1361
    }
1362
  }
1363

1364
  @Override
1365
  public void writeFileLines(List<String> content, Path file, boolean createParentDir) {
1366

1367
    if (createParentDir) {
2!
1368
      mkdirs(file.getParent());
×
1369
    }
1370
    if (content == null) {
2!
1371
      content = List.of();
×
1372
    }
1373
    LOG.trace("Writing content with {} lines to file {}", content.size(), file);
7✔
1374
    if (Files.exists(file)) {
5✔
1375
      LOG.debug("Overriding content of file {}", file);
4✔
1376
    }
1377
    try {
1378
      Files.write(file, content);
6✔
1379
      LOG.trace("Wrote lines to file {}", file);
4✔
1380
    } catch (IOException e) {
×
1381
      throw new RuntimeException("Failed to write file " + file, e);
×
1382
    }
1✔
1383
  }
1✔
1384

1385
  @Override
1386
  public void readProperties(Path file, Properties properties) {
1387

1388
    try (Reader reader = Files.newBufferedReader(file)) {
3✔
1389
      properties.load(reader);
3✔
1390
      LOG.debug("Successfully loaded {} properties from {}", properties.size(), file);
7✔
1391
    } catch (IOException e) {
×
1392
      throw new IllegalStateException("Failed to read properties file: " + file, e);
×
1393
    }
1✔
1394
  }
1✔
1395

1396
  @Override
1397
  public void writeProperties(Properties properties, Path file, boolean createParentDir) {
1398

1399
    if (createParentDir) {
2✔
1400
      mkdirs(file.getParent());
4✔
1401
    }
1402
    try (Writer writer = Files.newBufferedWriter(file)) {
5✔
1403
      properties.store(writer, null); // do not get confused - Java still writes a date/time header that cannot be omitted
4✔
1404
      LOG.debug("Successfully saved {} properties to {}", properties.size(), file);
7✔
1405
    } catch (IOException e) {
×
1406
      throw new IllegalStateException("Failed to save properties file during tests.", e);
×
1407
    }
1✔
1408
  }
1✔
1409

1410
  @Override
1411
  public void readIniFile(Path file, IniFile iniFile) {
1412

1413
    if (!Files.exists(file)) {
5✔
1414
      LOG.debug("INI file {} does not exist.", iniFile);
4✔
1415
      return;
1✔
1416
    }
1417
    List<String> iniLines = readFileLines(file);
4✔
1418
    IniSection currentIniSection = iniFile.getInitialSection();
3✔
1419
    for (String line : iniLines) {
10✔
1420
      if (line.trim().startsWith("[")) {
5✔
1421
        currentIniSection = iniFile.getOrCreateSection(line);
5✔
1422
      } else if (line.isBlank() || IniComment.COMMENT_SYMBOLS.contains(line.trim().charAt(0))) {
11✔
1423
        currentIniSection.addComment(line);
4✔
1424
      } else {
1425
        int index = line.indexOf('=');
4✔
1426
        if (index > 0) {
2!
1427
          currentIniSection.setProperty(line);
3✔
1428
        }
1429
      }
1430
    }
1✔
1431
  }
1✔
1432

1433
  @Override
1434
  public void writeIniFile(IniFile iniFile, Path file, boolean createParentDir) {
1435

1436
    String iniString = iniFile.toString();
3✔
1437
    writeFileContent(iniString, file, createParentDir);
5✔
1438
  }
1✔
1439

1440
  @Override
1441
  public Duration getFileAge(Path path) {
1442

1443
    if (Files.exists(path)) {
5✔
1444
      try {
1445
        long currentTime = System.currentTimeMillis();
2✔
1446
        long fileModifiedTime = Files.getLastModifiedTime(path).toMillis();
6✔
1447
        return Duration.ofMillis(currentTime - fileModifiedTime);
5✔
1448
      } catch (IOException e) {
×
1449
        LOG.warn("Could not get modification-time of {}.", path, e);
×
1450
      }
×
1451
    } else {
1452
      LOG.debug("Path {} is missing - skipping modification-time and file age check.", path);
4✔
1453
    }
1454
    return null;
2✔
1455
  }
1456

1457
  @Override
1458
  public boolean isFileAgeRecent(Path path, Duration cacheDuration) {
1459

1460
    Duration age = getFileAge(path);
4✔
1461
    if (age == null) {
2✔
1462
      return false;
2✔
1463
    }
1464
    LOG.debug("The path {} was last updated {} ago and caching duration is {}.", path, age, cacheDuration);
17✔
1465
    return (age.toMillis() <= cacheDuration.toMillis());
10✔
1466
  }
1467
}
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