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

devonfw / IDEasy / 20115895339

10 Dec 2025 10:55PM UTC coverage: 70.14% (+0.2%) from 69.924%
20115895339

push

github

web-flow
#1166 automatic project import for intellij #1508: fix xml merge when file is empty (#1649)

Co-authored-by: cthies <caroline.thies@capgemini.com>
Co-authored-by: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com>
Co-authored-by: jan-vcapgemini <jan-vincent.hoelzle@capgemini.com>

3963 of 6211 branches covered (63.81%)

Branch coverage included in aggregate %.

10138 of 13893 relevant lines covered (72.97%)

3.15 hits per line

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

67.43
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

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

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

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

75
  private static final String WINDOWS_FILE_LOCK_WARNING =
76
      "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"
77
          + WINDOWS_FILE_LOCK_DOCUMENTATION_PAGE;
78

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

81
  private final IdeContext context;
82

83
  /**
84
   * The constructor.
85
   *
86
   * @param context the {@link IdeContext} to use.
87
   */
88
  public FileAccessImpl(IdeContext context) {
89

90
    super();
2✔
91
    this.context = context;
3✔
92
  }
1✔
93

94
  @Override
95
  public void download(String url, Path target) {
96

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

112
  private void downloadViaHttp(String url, Path target) {
113

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

136
  private void downloadWithHttpVersion(String url, Path target, Version httpVersion) throws Exception {
137

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

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

160
    long contentLength = response.headers().firstValueAsLong("content-length").orElse(-1);
7✔
161
    if (contentLength < 0) {
4✔
162
      this.context.warning("Content-Length was not provided by download from {}", url);
10✔
163
    }
164

165
    byte[] data = new byte[1024];
3✔
166
    boolean fileComplete = false;
2✔
167
    int count;
168

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

187
  private void copyFileWithProgressBar(Path source, Path target) {
188

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

210
  @Override
211
  public String download(String url) {
212

213
    this.context.debug("Downloading text body from {}", url);
10✔
214
    return httpGetAsString(url);
3✔
215
  }
216

217
  @Override
218
  public void mkdirs(Path directory) {
219

220
    if (Files.isDirectory(directory)) {
5✔
221
      return;
1✔
222
    }
223
    this.context.trace("Creating directory {}", directory);
10✔
224
    try {
225
      Files.createDirectories(directory);
5✔
226
    } catch (IOException e) {
×
227
      throw new IllegalStateException("Failed to create directory " + directory, e);
×
228
    }
1✔
229
  }
1✔
230

231
  @Override
232
  public boolean isFile(Path file) {
233

234
    if (!Files.exists(file)) {
5!
235
      this.context.trace("File {} does not exist", file);
×
236
      return false;
×
237
    }
238
    if (Files.isDirectory(file)) {
5!
239
      this.context.trace("Path {} is a directory but a regular file was expected", file);
×
240
      return false;
×
241
    }
242
    return true;
2✔
243
  }
244

245
  @Override
246
  public boolean isExpectedFolder(Path folder) {
247

248
    if (Files.isDirectory(folder)) {
5✔
249
      return true;
2✔
250
    }
251
    this.context.warning("Expected folder was not found at {}", folder);
10✔
252
    return false;
2✔
253
  }
254

255
  @Override
256
  public String checksum(Path file, String hashAlgorithm) {
257

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

277
  @Override
278
  public boolean isJunction(Path path) {
279

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

301
  @Override
302
  public Path backup(Path fileOrFolder) {
303

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

324
  private static Path appendParentPath(Path path, Path parent, int max) {
325

326
    if ((parent == null) || (max <= 0)) {
4!
327
      return path;
2✔
328
    }
329
    return appendParentPath(path, parent.getParent(), max - 1).resolve(parent.getFileName());
11✔
330
  }
331

332
  @Override
333
  public void move(Path source, Path targetDir, StandardCopyOption... copyOptions) {
334

335
    this.context.trace("Moving {} to {}", source, targetDir);
14✔
336
    try {
337
      Files.move(source, targetDir, copyOptions);
5✔
338
    } catch (IOException e) {
×
339
      String fileType = Files.isSymbolicLink(source) ? "symlink" : isJunction(source) ? "junction" : Files.isDirectory(source) ? "directory" : "file";
×
340
      String message = "Failed to move " + fileType + ": " + source + " to " + targetDir + ".";
×
341
      if (this.context.getSystemInfo().isWindows()) {
×
342
        message = message + "\n" + WINDOWS_FILE_LOCK_WARNING;
×
343
      }
344
      throw new IllegalStateException(message, e);
×
345
    }
1✔
346
  }
1✔
347

348
  @Override
349
  public void copy(Path source, Path target, FileCopyMode mode, PathCopyListener listener) {
350

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

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

395
    if (Files.isDirectory(source)) {
5✔
396
      mkdirs(target);
3✔
397
      try (Stream<Path> childStream = Files.list(source)) {
3✔
398
        Iterator<Path> iterator = childStream.iterator();
3✔
399
        while (iterator.hasNext()) {
3✔
400
          Path child = iterator.next();
4✔
401
          copyRecursive(child, target.resolve(child.getFileName().toString()), mode, listener);
10✔
402
        }
1✔
403
      }
404
      listener.onCopy(source, target, true);
6✔
405
    } else if (Files.exists(source)) {
5!
406
      if (mode.isOverride()) {
3✔
407
        delete(target);
3✔
408
      }
409
      this.context.trace("Starting to {} {} to {}", mode.getOperation(), source, target);
19✔
410
      Files.copy(source, target);
6✔
411
      listener.onCopy(source, target, false);
6✔
412
    } else {
413
      throw new IOException("Path " + source + " does not exist.");
×
414
    }
415
  }
1✔
416

417
  /**
418
   * 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
419
   * {@link Path} that is neither a symbolic link nor a Windows junction.
420
   *
421
   * @param path the {@link Path} to delete.
422
   */
423
  private void deleteLinkIfExists(Path path) {
424

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

428
    assert !(isSymlink && isJunction);
5!
429

430
    if (isJunction || isSymlink) {
4!
431
      this.context.info("Deleting previous " + (isJunction ? "junction" : "symlink") + " at " + path);
9!
432
      try {
433
        Files.delete(path);
2✔
434
      } catch (IOException e) {
×
435
        throw new IllegalStateException("Failed to delete link at " + path, e);
×
436
      }
1✔
437
    }
438
  }
1✔
439

440
  /**
441
   * Adapts the given {@link Path} to be relative or absolute depending on the given {@code relative} flag. Additionally, {@link Path#toRealPath(LinkOption...)}
442
   * is applied to {@code target}.
443
   *
444
   * @param target the {@link Path} the link should point to and that is to be adapted.
445
   * @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}.
446
   * @param relative the {@code relative} flag.
447
   * @return the adapted {@link Path}.
448
   * @see #symlink(Path, Path, boolean)
449
   */
450
  private Path adaptPath(Path target, Path link, boolean relative) {
451

452
    if (!target.isAbsolute()) {
3✔
453
      target = link.resolveSibling(target);
4✔
454
    }
455
    try {
456
      target = target.toRealPath(LinkOption.NOFOLLOW_LINKS); // to transform ../d1/../d2 to ../d2
9✔
457
    } catch (IOException e) {
×
458
      throw new RuntimeException("Failed to get real path of " + target, e);
×
459
    }
1✔
460
    if (relative) {
2✔
461
      target = link.getParent().relativize(target);
5✔
462
      // to make relative links like this work: dir/link -> dir
463
      target = (target.toString().isEmpty()) ? Path.of(".") : target;
11✔
464
    }
465
    return target;
2✔
466
  }
467

468
  /**
469
   * Creates a Windows link using mklink at {@code link} pointing to {@code target}.
470
   *
471
   * @param target the {@link Path} the link will point to.
472
   * @param link the {@link Path} where to create the link.
473
   * @param type the {@link PathLinkType}.
474
   */
475
  private void mklinkOnWindows(Path target, Path link, PathLinkType type) {
476

477
    this.context.trace("Creating a Windows {} at {} pointing to {}", type, link, target);
×
478
    ProcessContext pc = this.context.newProcess().executable("cmd").addArgs("/c", "mklink", type.getMklinkOption());
×
479
    Path parent = link.getParent();
×
480
    if (parent == null) {
×
481
      parent = Path.of(".");
×
482
    }
483
    Path absolute = parent.resolve(target).toAbsolutePath().normalize();
×
484
    if (type == PathLinkType.SYMBOLIC_LINK && Files.isDirectory(absolute)) {
×
485
      pc = pc.addArg("/j");
×
486
      target = absolute;
×
487
    }
488
    pc = pc.addArgs(link.toString(), target.toString());
×
489
    ProcessResult result = pc.run(ProcessMode.DEFAULT);
×
490
    result.failOnError();
×
491
  }
×
492

493
  @Override
494
  public void link(Path source, Path link, boolean relative, PathLinkType type) {
495

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

528
  @Override
529
  public Path toRealPath(Path path) {
530

531
    return toRealPath(path, true);
5✔
532
  }
533

534
  @Override
535
  public Path toCanonicalPath(Path path) {
536

537
    return toRealPath(path, false);
5✔
538
  }
539

540
  private Path toRealPath(Path path, boolean resolveLinks) {
541

542
    try {
543
      Path realPath;
544
      if (resolveLinks) {
2✔
545
        realPath = path.toRealPath();
6✔
546
      } else {
547
        realPath = path.toRealPath(LinkOption.NOFOLLOW_LINKS);
9✔
548
      }
549
      if (!realPath.equals(path)) {
4✔
550
        this.context.trace("Resolved path {} to {}", path, realPath);
14✔
551
      }
552
      return realPath;
2✔
553
    } catch (IOException e) {
×
554
      throw new IllegalStateException("Failed to get real path for " + path, e);
×
555
    }
556
  }
557

558
  @Override
559
  public Path createTempDir(String name) {
560

561
    try {
562
      Path tmp = this.context.getTempPath();
4✔
563
      Path tempDir = tmp.resolve(name);
4✔
564
      int tries = 1;
2✔
565
      while (Files.exists(tempDir)) {
5!
566
        long id = System.nanoTime() & 0xFFFF;
×
567
        tempDir = tmp.resolve(name + "-" + id);
×
568
        tries++;
×
569
        if (tries > 200) {
×
570
          throw new IOException("Unable to create unique name!");
×
571
        }
572
      }
×
573
      return Files.createDirectory(tempDir);
5✔
574
    } catch (IOException e) {
×
575
      throw new IllegalStateException("Failed to create temporary directory with prefix '" + name + "'!", e);
×
576
    }
577
  }
578

579
  @Override
580
  public void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtractHook, boolean extract) {
581

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

620
  private void postExtractHook(Consumer<Path> postExtractHook, Path properInstallDir) {
621

622
    if (postExtractHook != null) {
2✔
623
      postExtractHook.accept(properInstallDir);
3✔
624
    }
625
  }
1✔
626

627
  /**
628
   * @param path the {@link Path} to start the recursive search from.
629
   * @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
630
   *     item in their respective directory and {@code s} is not named "bin".
631
   */
632
  private Path getProperInstallationSubDirOf(Path path, Path archiveFile) {
633

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

651
  @Override
652
  public void extractZip(Path file, Path targetDir) {
653

654
    this.context.info("Extracting ZIP file {} to {}", file, targetDir);
14✔
655
    URI uri = URI.create("jar:" + file.toUri());
6✔
656
    try (FileSystem fs = FileSystems.newFileSystem(uri, FS_ENV)) {
4✔
657
      long size = 0;
2✔
658
      for (Path root : fs.getRootDirectories()) {
11✔
659
        size += getFileSizeRecursive(root);
6✔
660
      }
1✔
661
      try (final IdeProgressBar progressBar = this.context.newProgressbarForExtracting(size)) {
5✔
662
        for (Path root : fs.getRootDirectories()) {
11✔
663
          copy(root, targetDir, FileCopyMode.EXTRACT, (s, t, d) -> onFileCopiedFromZip(s, t, d, progressBar));
15✔
664
        }
1✔
665
      }
666
    } catch (IOException e) {
×
667
      throw new IllegalStateException("Failed to extract " + file + " to " + targetDir, e);
×
668
    }
1✔
669
  }
1✔
670

671
  @SuppressWarnings("unchecked")
672
  private void onFileCopiedFromZip(Path source, Path target, boolean directory, IdeProgressBar progressBar) {
673

674
    if (directory) {
2✔
675
      return;
1✔
676
    }
677
    if (!this.context.getSystemInfo().isWindows()) {
5✔
678
      try {
679
        Object attribute = Files.getAttribute(source, "zip:permissions");
6✔
680
        if (attribute instanceof Set<?> permissionSet) {
6✔
681
          Files.setPosixFilePermissions(target, (Set<PosixFilePermission>) permissionSet);
4✔
682
        }
683
      } catch (Exception e) {
×
684
        this.context.error(e, "Failed to transfer zip permissions for {}", target);
×
685
      }
1✔
686
    }
687
    progressBar.stepBy(getFileSize(target));
5✔
688
  }
1✔
689

690
  @Override
691
  public void extractTar(Path file, Path targetDir, TarCompression compression) {
692

693
    extractArchive(file, targetDir, in -> new TarArchiveInputStream(compression.unpack(in)));
13✔
694
  }
1✔
695

696
  @Override
697
  public void extractJar(Path file, Path targetDir) {
698

699
    extractZip(file, targetDir);
4✔
700
  }
1✔
701

702
  /**
703
   * @param permissions The integer as returned by {@link TarArchiveEntry#getMode()} that represents the file permissions of a file on a Unix file system.
704
   * @return A String representing the file permissions. E.g. "rwxrwxr-x" or "rw-rw-r--"
705
   */
706
  public static String generatePermissionString(int permissions) {
707

708
    // Ensure that only the last 9 bits are considered
709
    permissions &= 0b111111111;
4✔
710

711
    StringBuilder permissionStringBuilder = new StringBuilder("rwxrwxrwx");
5✔
712
    for (int i = 0; i < 9; i++) {
7✔
713
      int mask = 1 << i;
4✔
714
      char currentChar = ((permissions & mask) != 0) ? permissionStringBuilder.charAt(8 - i) : '-';
12✔
715
      permissionStringBuilder.setCharAt(8 - i, currentChar);
6✔
716
    }
717

718
    return permissionStringBuilder.toString();
3✔
719
  }
720

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

723
    this.context.info("Extracting TAR file {} to {}", file, targetDir);
14✔
724

725
    final List<PathLink> links = new ArrayList<>();
4✔
726
    try (InputStream is = Files.newInputStream(file);
5✔
727
        ArchiveInputStream<?> ais = unpacker.apply(is);
5✔
728
        IdeProgressBar pb = this.context.newProgressbarForExtracting(getFileSize(file))) {
7✔
729

730
      final Path root = targetDir.toAbsolutePath().normalize();
4✔
731

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

777
  private Path resolveRelativePathSecure(String entryName, Path root) {
778

779
    Path entryPath = root.resolve(entryName).normalize();
5✔
780
    return resolveRelativePathSecure(entryPath, root, entryName);
6✔
781
  }
782

783
  private Path resolveRelativePathSecure(Path entryPath, Path root, String entryName) {
784

785
    if (!entryPath.startsWith(root)) {
4!
786
      throw new IllegalStateException("Preventing path traversal attack from " + entryName + " to " + entryPath + " leaving " + root);
×
787
    }
788
    return entryPath;
2✔
789
  }
790

791
  @Override
792
  public void extractDmg(Path file, Path targetDir) {
793

794
    this.context.info("Extracting DMG file {} to {}", file, targetDir);
×
795
    assert this.context.getSystemInfo().isMac();
×
796

797
    Path mountPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES).resolve(IdeContext.FOLDER_VOLUME);
×
798
    mkdirs(mountPath);
×
799
    ProcessContext pc = this.context.newProcess();
×
800
    pc.executable("hdiutil");
×
801
    pc.addArgs("attach", "-quiet", "-nobrowse", "-mountpoint", mountPath, file);
×
802
    pc.run();
×
803
    Path appPath = findFirst(mountPath, p -> p.getFileName().toString().endsWith(".app"), false);
×
804
    if (appPath == null) {
×
805
      throw new IllegalStateException("Failed to unpack DMG as no MacOS *.app was found in file " + file);
×
806
    }
807

808
    copy(appPath, targetDir, FileCopyMode.COPY_TREE_OVERRIDE_TREE);
×
809
    pc.addArgs("detach", "-force", mountPath);
×
810
    pc.run();
×
811
  }
×
812

813
  @Override
814
  public void extractMsi(Path file, Path targetDir) {
815

816
    this.context.info("Extracting MSI file {} to {}", file, targetDir);
×
817
    this.context.newProcess().executable("msiexec").addArgs("/a", file, "/qn", "TARGETDIR=" + targetDir).run();
×
818
    // msiexec also creates a copy of the MSI
819
    Path msiCopy = targetDir.resolve(file.getFileName());
×
820
    delete(msiCopy);
×
821
  }
×
822

823
  @Override
824
  public void extractPkg(Path file, Path targetDir) {
825

826
    this.context.info("Extracting PKG file {} to {}", file, targetDir);
×
827
    Path tmpDirPkg = createTempDir("ide-pkg-");
×
828
    ProcessContext pc = this.context.newProcess();
×
829
    // we might also be able to use cpio from commons-compression instead of external xar...
830
    pc.executable("xar").addArgs("-C", tmpDirPkg, "-xf", file).run();
×
831
    Path contentPath = findFirst(tmpDirPkg, p -> p.getFileName().toString().equals("Payload"), true);
×
832
    extractTar(contentPath, targetDir, TarCompression.GZ);
×
833
    delete(tmpDirPkg);
×
834
  }
×
835

836
  @Override
837
  public void compress(Path dir, OutputStream out, String path) {
838

839
    String extension = FilenameUtil.getExtension(path);
3✔
840
    TarCompression tarCompression = TarCompression.of(extension);
3✔
841
    if (tarCompression != null) {
2!
842
      compressTar(dir, out, tarCompression);
6✔
843
    } else if (extension.equals("zip")) {
×
844
      compressZip(dir, out);
×
845
    } else {
846
      throw new IllegalArgumentException("Unsupported extension: " + extension);
×
847
    }
848
  }
1✔
849

850
  @Override
851
  public void compressTar(Path dir, OutputStream out, TarCompression tarCompression) {
852

853
    switch (tarCompression) {
8!
854
      case null -> compressTar(dir, out);
×
855
      case NONE -> compressTar(dir, out);
×
856
      case GZ -> compressTarGz(dir, out);
5✔
857
      case BZIP2 -> compressTarBzip2(dir, out);
×
858
      default -> throw new IllegalArgumentException("Unsupported tar compression: " + tarCompression);
×
859
    }
860
  }
1✔
861

862
  @Override
863
  public void compressTarGz(Path dir, OutputStream out) {
864

865
    try (GzipCompressorOutputStream gzOut = new GzipCompressorOutputStream(out)) {
5✔
866
      compressTarOrThrow(dir, gzOut);
4✔
867
    } catch (IOException e) {
×
868
      throw new IllegalStateException("Failed to compress directory " + dir + " to tar.gz file.", e);
×
869
    }
1✔
870
  }
1✔
871

872
  @Override
873
  public void compressTarBzip2(Path dir, OutputStream out) {
874

875
    try (BZip2CompressorOutputStream bzip2Out = new BZip2CompressorOutputStream(out)) {
×
876
      compressTarOrThrow(dir, bzip2Out);
×
877
    } catch (IOException e) {
×
878
      throw new IllegalStateException("Failed to compress directory " + dir + " to tar.bz2 file.", e);
×
879
    }
×
880
  }
×
881

882
  @Override
883
  public void compressTar(Path dir, OutputStream out) {
884

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

892
  private void compressTarOrThrow(Path dir, OutputStream out) throws IOException {
893

894
    try (TarArchiveOutputStream tarOut = new TarArchiveOutputStream(out)) {
5✔
895
      compressRecursive(dir, tarOut, "");
5✔
896
      tarOut.finish();
2✔
897
    }
898
  }
1✔
899

900
  @Override
901
  public void compressZip(Path dir, OutputStream out) {
902

903
    try (ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(out)) {
×
904
      compressRecursive(dir, zipOut, "");
×
905
      zipOut.finish();
×
906
    } catch (IOException e) {
×
907
      throw new IllegalStateException("Failed to compress directory " + dir + " to zip file.", e);
×
908
    }
×
909
  }
×
910

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

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

949
  @Override
950
  public void delete(Path path) {
951

952
    if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
9✔
953
      this.context.trace("Deleting {} skipped as the path does not exist.", path);
10✔
954
      return;
1✔
955
    }
956
    this.context.debug("Deleting {} ...", path);
10✔
957
    try {
958
      if (Files.isSymbolicLink(path) || isJunction(path)) {
7!
959
        Files.delete(path);
3✔
960
      } else {
961
        deleteRecursive(path);
3✔
962
      }
963
    } catch (IOException e) {
×
964
      throw new IllegalStateException("Failed to delete " + path, e);
×
965
    }
1✔
966
  }
1✔
967

968
  private void deleteRecursive(Path path) throws IOException {
969

970
    if (Files.isDirectory(path)) {
5✔
971
      try (Stream<Path> childStream = Files.list(path)) {
3✔
972
        Iterator<Path> iterator = childStream.iterator();
3✔
973
        while (iterator.hasNext()) {
3✔
974
          Path child = iterator.next();
4✔
975
          deleteRecursive(child);
3✔
976
        }
1✔
977
      }
978
    }
979
    this.context.trace("Deleting {} ...", path);
10✔
980
    boolean isSetWritable = setWritable(path, true);
5✔
981
    if (!isSetWritable) {
2✔
982
      this.context.debug("Couldn't give write access to file: " + path);
6✔
983
    }
984
    Files.delete(path);
2✔
985
  }
1✔
986

987
  @Override
988
  public Path findFirst(Path dir, Predicate<Path> filter, boolean recursive) {
989

990
    try {
991
      if (!Files.isDirectory(dir)) {
5!
992
        return null;
×
993
      }
994
      return findFirstRecursive(dir, filter, recursive);
6✔
995
    } catch (IOException e) {
×
996
      throw new IllegalStateException("Failed to search for file in " + dir, e);
×
997
    }
998
  }
999

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

1002
    List<Path> folders = null;
2✔
1003
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
1004
      Iterator<Path> iterator = childStream.iterator();
3✔
1005
      while (iterator.hasNext()) {
3✔
1006
        Path child = iterator.next();
4✔
1007
        if (filter.test(child)) {
4✔
1008
          return child;
4✔
1009
        } else if (recursive && Files.isDirectory(child)) {
2!
1010
          if (folders == null) {
×
1011
            folders = new ArrayList<>();
×
1012
          }
1013
          folders.add(child);
×
1014
        }
1015
      }
1✔
1016
    }
4!
1017
    if (folders != null) {
2!
1018
      for (Path child : folders) {
×
1019
        Path match = findFirstRecursive(child, filter, recursive);
×
1020
        if (match != null) {
×
1021
          return match;
×
1022
        }
1023
      }
×
1024
    }
1025
    return null;
2✔
1026
  }
1027

1028
  @Override
1029
  public Path findAncestor(Path path, Path baseDir, int subfolderCount) {
1030

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

1064
  @Override
1065
  public List<Path> listChildrenMapped(Path dir, Function<Path, Path> filter) {
1066

1067
    if (!Files.isDirectory(dir)) {
5✔
1068
      return List.of();
2✔
1069
    }
1070
    List<Path> children = new ArrayList<>();
4✔
1071
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
1072
      Iterator<Path> iterator = childStream.iterator();
3✔
1073
      while (iterator.hasNext()) {
3✔
1074
        Path child = iterator.next();
4✔
1075
        Path filteredChild = filter.apply(child);
5✔
1076
        if (filteredChild != null) {
2✔
1077
          if (filteredChild == child) {
3!
1078
            this.context.trace("Accepted file {}", child);
11✔
1079
          } else {
1080
            this.context.trace("Accepted file {} and mapped to {}", child, filteredChild);
×
1081
          }
1082
          children.add(filteredChild);
5✔
1083
        } else {
1084
          this.context.trace("Ignoring file {} according to filter", child);
10✔
1085
        }
1086
      }
1✔
1087
    } catch (IOException e) {
×
1088
      throw new IllegalStateException("Failed to find children of directory " + dir, e);
×
1089
    }
1✔
1090
    return children;
2✔
1091
  }
1092

1093
  @Override
1094
  public boolean isEmptyDir(Path dir) {
1095

1096
    return listChildren(dir, f -> true).isEmpty();
8✔
1097
  }
1098

1099
  @Override
1100
  public boolean isNonEmptyFile(Path file) {
1101

1102
    if (Files.isRegularFile(file)) {
5✔
1103
      return (getFileSize(file) > 0);
10✔
1104
    }
1105
    return false;
2✔
1106
  }
1107

1108
  private long getFileSize(Path file) {
1109

1110
    try {
1111
      return Files.size(file);
3✔
1112
    } catch (IOException e) {
×
1113
      this.context.warning(e.getMessage(), e);
×
1114
      return 0;
×
1115
    }
1116
  }
1117

1118
  private long getFileSizeRecursive(Path path) {
1119

1120
    long size = 0;
2✔
1121
    if (Files.isDirectory(path)) {
5✔
1122
      try (Stream<Path> childStream = Files.list(path)) {
3✔
1123
        Iterator<Path> iterator = childStream.iterator();
3✔
1124
        while (iterator.hasNext()) {
3✔
1125
          Path child = iterator.next();
4✔
1126
          size += getFileSizeRecursive(child);
6✔
1127
        }
1✔
1128
      } catch (IOException e) {
×
1129
        throw new RuntimeException("Failed to iterate children of folder " + path, e);
×
1130
      }
1✔
1131
    } else {
1132
      size += getFileSize(path);
6✔
1133
    }
1134
    return size;
2✔
1135
  }
1136

1137
  @Override
1138
  public Path findExistingFile(String fileName, List<Path> searchDirs) {
1139

1140
    for (Path dir : searchDirs) {
10!
1141
      Path filePath = dir.resolve(fileName);
4✔
1142
      try {
1143
        if (Files.exists(filePath)) {
5✔
1144
          return filePath;
2✔
1145
        }
1146
      } catch (Exception e) {
×
1147
        throw new IllegalStateException("Unexpected error while checking existence of file " + filePath + " .", e);
×
1148
      }
1✔
1149
    }
1✔
1150
    return null;
×
1151
  }
1152

1153
  @Override
1154
  public boolean setWritable(Path file, boolean writable) {
1155

1156
    try {
1157
      // POSIX
1158
      PosixFileAttributeView posix = Files.getFileAttributeView(file, PosixFileAttributeView.class);
7✔
1159
      if (posix != null) {
2!
1160
        Set<PosixFilePermission> permissions = new HashSet<>(posix.readAttributes().permissions());
7✔
1161
        boolean changed;
1162
        if (writable) {
2!
1163
          changed = permissions.add(PosixFilePermission.OWNER_WRITE);
5✔
1164
        } else {
1165
          changed = permissions.remove(PosixFilePermission.OWNER_WRITE);
×
1166
        }
1167
        if (changed) {
2!
1168
          posix.setPermissions(permissions);
×
1169
        }
1170
        return true;
2✔
1171
      }
1172

1173
      // Windows
1174
      DosFileAttributeView dos = Files.getFileAttributeView(file, DosFileAttributeView.class);
×
1175
      if (dos != null) {
×
1176
        dos.setReadOnly(!writable);
×
1177
        return true;
×
1178
      }
1179

1180
      this.context.debug("Failed to set writing permission for file {}", file);
×
1181
      return false;
×
1182

1183
    } catch (IOException e) {
1✔
1184
      this.context.debug("Error occurred when trying to set writing permission for file " + file + ": " + e);
8✔
1185
      return false;
2✔
1186
    }
1187
  }
1188

1189
  @Override
1190
  public void makeExecutable(Path path, boolean confirm) {
1191

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

1222
  @Override
1223
  public PathPermissions getFilePermissions(Path path) {
1224

1225
    PathPermissions pathPermissions;
1226
    String info = "";
2✔
1227
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1228
      info = "mocked-";
×
1229
      if (Files.isDirectory(path)) {
×
1230
        pathPermissions = PathPermissions.MODE_RWX_RX_RX;
×
1231
      } else {
1232
        Path parent = path.getParent();
×
1233
        if ((parent != null) && (parent.getFileName().toString().equals("bin"))) {
×
1234
          pathPermissions = PathPermissions.MODE_RWX_RX_RX;
×
1235
        } else {
1236
          pathPermissions = PathPermissions.MODE_RW_R_R;
×
1237
        }
1238
      }
×
1239
    } else {
1240
      Set<PosixFilePermission> permissions;
1241
      try {
1242
        // Read the current file permissions
1243
        permissions = Files.getPosixFilePermissions(path);
5✔
1244
      } catch (IOException e) {
×
1245
        throw new RuntimeException("Failed to get permissions for " + path, e);
×
1246
      }
1✔
1247
      pathPermissions = PathPermissions.of(permissions);
3✔
1248
    }
1249
    this.context.trace("Read {}permissions of {} as {}.", info, path, pathPermissions);
18✔
1250
    return pathPermissions;
2✔
1251
  }
1252

1253
  @Override
1254
  public void setFilePermissions(Path path, PathPermissions permissions, boolean logErrorAndContinue) {
1255

1256
    if (skipPermissionsIfWindows(path)) {
4!
1257
      return;
×
1258
    }
1259
    try {
1260
      this.context.debug("Setting permissions for {} to {}", path, permissions);
14✔
1261
      // Set the new permissions
1262
      Files.setPosixFilePermissions(path, permissions.toPosix());
5✔
1263
    } catch (IOException e) {
×
1264
      if (logErrorAndContinue) {
×
1265
        this.context.warning().log(e, "Failed to set permissions to {} for path {}", permissions, path);
×
1266
      } else {
1267
        throw new RuntimeException("Failed to set permissions to " + permissions + " for path " + path, e);
×
1268
      }
1269
    }
1✔
1270
  }
1✔
1271

1272
  private boolean skipPermissionsIfWindows(Path path) {
1273

1274
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1275
      this.context.trace("Windows does not have file permissions hence omitting for {}", path);
×
1276
      return true;
×
1277
    }
1278
    return false;
2✔
1279
  }
1280

1281
  @Override
1282
  public void touch(Path file) {
1283

1284
    if (Files.exists(file)) {
5✔
1285
      try {
1286
        Files.setLastModifiedTime(file, FileTime.fromMillis(System.currentTimeMillis()));
5✔
1287
      } catch (IOException e) {
×
1288
        throw new IllegalStateException("Could not update modification-time of " + file, e);
×
1289
      }
1✔
1290
    } else {
1291
      try {
1292
        Files.createFile(file);
5✔
1293
      } catch (IOException e) {
1✔
1294
        throw new IllegalStateException("Could not create empty file " + file, e);
8✔
1295
      }
1✔
1296
    }
1297
  }
1✔
1298

1299
  @Override
1300
  public String readFileContent(Path file) {
1301

1302
    this.context.trace("Reading content of file from {}", file);
10✔
1303
    if (!Files.exists((file))) {
5!
1304
      this.context.debug("File {} does not exist", file);
×
1305
      return null;
×
1306
    }
1307
    try {
1308
      String content = Files.readString(file);
3✔
1309
      this.context.trace("Completed reading {} character(s) from file {}", content.length(), file);
16✔
1310
      return content;
2✔
1311
    } catch (IOException e) {
×
1312
      throw new IllegalStateException("Failed to read file " + file, e);
×
1313
    }
1314
  }
1315

1316
  @Override
1317
  public void writeFileContent(String content, Path file, boolean createParentDir) {
1318

1319
    if (createParentDir) {
2✔
1320
      mkdirs(file.getParent());
4✔
1321
    }
1322
    if (content == null) {
2!
1323
      content = "";
×
1324
    }
1325
    this.context.trace("Writing content with {} character(s) to file {}", content.length(), file);
16✔
1326
    if (Files.exists(file)) {
5✔
1327
      this.context.info("Overriding content of file {}", file);
10✔
1328
    }
1329
    try {
1330
      Files.writeString(file, content);
6✔
1331
      this.context.trace("Wrote content to file {}", file);
10✔
1332
    } catch (IOException e) {
×
1333
      throw new RuntimeException("Failed to write file " + file, e);
×
1334
    }
1✔
1335
  }
1✔
1336

1337
  @Override
1338
  public List<String> readFileLines(Path file) {
1339

1340
    this.context.trace("Reading content of file from {}", file);
10✔
1341
    if (!Files.exists(file)) {
5✔
1342
      this.context.warning("File {} does not exist", file);
10✔
1343
      return null;
2✔
1344
    }
1345
    try {
1346
      List<String> content = Files.readAllLines(file);
3✔
1347
      this.context.trace("Completed reading {} lines from file {}", content.size(), file);
16✔
1348
      return content;
2✔
1349
    } catch (IOException e) {
×
1350
      throw new IllegalStateException("Failed to read file " + file, e);
×
1351
    }
1352
  }
1353

1354
  @Override
1355
  public void writeFileLines(List<String> content, Path file, boolean createParentDir) {
1356

1357
    if (createParentDir) {
2!
1358
      mkdirs(file.getParent());
×
1359
    }
1360
    if (content == null) {
2!
1361
      content = List.of();
×
1362
    }
1363
    this.context.trace("Writing content with {} lines to file {}", content.size(), file);
16✔
1364
    if (Files.exists(file)) {
5✔
1365
      this.context.debug("Overriding content of file {}", file);
10✔
1366
    }
1367
    try {
1368
      Files.write(file, content);
6✔
1369
      this.context.trace("Wrote content to file {}", file);
10✔
1370
    } catch (IOException e) {
×
1371
      throw new RuntimeException("Failed to write file " + file, e);
×
1372
    }
1✔
1373
  }
1✔
1374

1375
  @Override
1376
  public void readProperties(Path file, Properties properties) {
1377

1378
    try (Reader reader = Files.newBufferedReader(file)) {
3✔
1379
      properties.load(reader);
3✔
1380
      this.context.debug("Successfully loaded {} properties from {}", properties.size(), file);
16✔
1381
    } catch (IOException e) {
×
1382
      throw new IllegalStateException("Failed to read properties file: " + file, e);
×
1383
    }
1✔
1384
  }
1✔
1385

1386
  @Override
1387
  public void writeProperties(Properties properties, Path file, boolean createParentDir) {
1388

1389
    if (createParentDir) {
2✔
1390
      mkdirs(file.getParent());
4✔
1391
    }
1392
    try (Writer writer = Files.newBufferedWriter(file)) {
5✔
1393
      properties.store(writer, null); // do not get confused - Java still writes a date/time header that cannot be omitted
4✔
1394
      this.context.debug("Successfully saved {} properties to {}", properties.size(), file);
16✔
1395
    } catch (IOException e) {
×
1396
      throw new IllegalStateException("Failed to save properties file during tests.", e);
×
1397
    }
1✔
1398
  }
1✔
1399

1400
  @Override
1401
  public void readIniFile(Path file, IniFile iniFile) {
1402

1403
    if (!Files.exists(file)) {
5✔
1404
      this.context.debug("INI file {} does not exist.", iniFile);
10✔
1405
      return;
1✔
1406
    }
1407
    List<String> iniLines = readFileLines(file);
4✔
1408
    IniSection currentIniSection = iniFile.getInitialSection();
3✔
1409
    for (String line : iniLines) {
10✔
1410
      if (line.trim().startsWith("[")) {
5✔
1411
        currentIniSection = iniFile.getOrCreateSection(line);
5✔
1412
      } else if (line.isBlank() || IniComment.COMMENT_SYMBOLS.contains(line.trim().charAt(0))) {
11✔
1413
        currentIniSection.addComment(line);
4✔
1414
      } else {
1415
        int index = line.indexOf('=');
4✔
1416
        if (index > 0) {
2!
1417
          currentIniSection.setProperty(line);
3✔
1418
        }
1419
      }
1420
    }
1✔
1421
  }
1✔
1422

1423
  @Override
1424
  public void writeIniFile(IniFile iniFile, Path file, boolean createParentDir) {
1425

1426
    String iniString = iniFile.toString();
3✔
1427
    writeFileContent(iniString, file, createParentDir);
5✔
1428
  }
1✔
1429

1430
  @Override
1431
  public Duration getFileAge(Path path) {
1432

1433
    if (Files.exists(path)) {
5✔
1434
      try {
1435
        long currentTime = System.currentTimeMillis();
2✔
1436
        long fileModifiedTime = Files.getLastModifiedTime(path).toMillis();
6✔
1437
        return Duration.ofMillis(currentTime - fileModifiedTime);
5✔
1438
      } catch (IOException e) {
×
1439
        this.context.warning().log(e, "Could not get modification-time of {}.", path);
×
1440
      }
×
1441
    } else {
1442
      this.context.debug("Path {} is missing - skipping modification-time and file age check.", path);
10✔
1443
    }
1444
    return null;
2✔
1445
  }
1446

1447
  @Override
1448
  public boolean isFileAgeRecent(Path path, Duration cacheDuration) {
1449

1450
    Duration age = getFileAge(path);
4✔
1451
    if (age == null) {
2✔
1452
      return false;
2✔
1453
    }
1454
    this.context.debug("The path {} was last updated {} ago and caching duration is {}.", path, age, cacheDuration);
18✔
1455
    return (age.toMillis() <= cacheDuration.toMillis());
10✔
1456
  }
1457
}
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