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

devonfw / IDEasy / 25378019331

05 May 2026 01:03PM UTC coverage: 71.041% (+0.4%) from 70.657%
25378019331

Pull #1826

github

web-flow
Merge eb21f872f into 2e54dfef7
Pull Request #1826: Add deterministic archive generation and checksum verification tests

4422 of 6880 branches covered (64.27%)

Branch coverage included in aggregate %.

11413 of 15410 relevant lines covered (74.06%)

3.13 hits per line

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

69.42
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.Objects;
37
import java.util.Properties;
38
import java.util.Set;
39
import java.util.function.Consumer;
40
import java.util.function.Function;
41
import java.util.function.Predicate;
42
import java.util.stream.Stream;
43

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

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

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

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

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

82
  private static final String WINDOWS_FILE_LOCK_WARNING =
83
      "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"
84
          + WINDOWS_FILE_LOCK_DOCUMENTATION_PAGE;
85

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

88
  private final IdeContext context;
89

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

97
    super();
2✔
98
    this.context = context;
3✔
99
  }
1✔
100

101
  @Override
102
  public void download(String url, Path target) {
103

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

119
  private void downloadViaHttp(String url, Path target) {
120

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

143
  private void downloadWithHttpVersion(String url, Path target, Version httpVersion) throws Exception {
144

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

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

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

172
    byte[] data = new byte[1024];
3✔
173
    boolean fileComplete = false;
2✔
174
    int count;
175

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

194
  private void copyFileWithProgressBar(Path source, Path target) {
195

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

217
  @Override
218
  public String download(String url) {
219

220
    LOG.debug("Downloading text body from {}", url);
4✔
221
    return httpGetAsString(url);
3✔
222
  }
223

224
  @Override
225
  public void mkdirs(Path directory) {
226

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

238
  @Override
239
  public boolean isFile(Path file) {
240

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

252
  @Override
253
  public boolean isExpectedFolder(Path folder) {
254

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

262
  @Override
263
  public String checksum(Path file, String hashAlgorithm) {
264

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

284
  @Override
285
  public boolean isJunction(Path path) {
286

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

308
  @Override
309
  public Path backup(Path fileOrFolder) {
310

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

336
  private static Path appendParentPath(Path path, Path parent, int max) {
337

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

344
  @Override
345
  public void move(Path source, Path targetDir, StandardCopyOption... copyOptions) {
346

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

360
  @Override
361
  public void copy(Path source, Path target, FileCopyMode mode, PathCopyListener listener) {
362

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

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

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

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

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

440
    assert !(isSymlink && isJunction);
5!
441

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

452
  /**
453
   * Adapts the given {@link Path} to be relative or absolute depending on the given {@code relative} flag. Additionally, {@link Path#toRealPath(LinkOption...)}
454
   * is applied to {@code target}.
455
   *
456
   * @param link the {@link Path} the link should point to and that is to be adapted.
457
   * @param source the {@link Path} to the link. It is used to calculate the relative path to the {@code target} if {@code relative} is set to
458
   *     {@code true}.
459
   * @param relative the {@code relative} flag.
460
   * @return the adapted {@link Path}.
461
   * @see #symlink(Path, Path, boolean)
462
   */
463
  private Path adaptPath(Path source, Path link, boolean relative) {
464

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

481
  /**
482
   * Creates a Windows link using mklink at {@code link} pointing to {@code target}.
483
   *
484
   * @param source the {@link Path} the link will point to.
485
   * @param link the {@link Path} where to create the link.
486
   * @param type the {@link PathLinkType}.
487
   */
488
  private void mklinkOnWindows(Path source, Path absoluteSource, Path link, PathLinkType type, boolean relative) {
489

490
    Path finalSource = source;
×
491
    Path finalLink = link;
×
492
    Path cwd = null;
×
493
    if (relative) {
×
494
      int count = getCommonNameCount(absoluteSource, link);
×
495
      if (count < 1) {
×
496
        LOG.error("Cannot create relative link at {} pointing to {} - falling back to absolute link", link, absoluteSource);
×
497
      } else {
498
        cwd = getPathStart(absoluteSource, count - 1);
×
499
        finalSource = getPathEnd(absoluteSource, count);
×
500
        finalLink = getPathEnd(link, count);
×
501
      }
502
    }
503

504
    String option = type.getMklinkOption();
×
505
    if (type == PathLinkType.SYMBOLIC_LINK) {
×
506
      boolean directoryTarget = Files.isDirectory(absoluteSource);
×
507
      option = directoryTarget ? "/j" : "/d";
×
508
    }
509

510
    if (!runMklink(finalSource, finalLink, cwd, option)) {
×
511
      throw new IllegalStateException("Failed to create Windows link at " + link + " pointing to " + source);
×
512
    }
513
  }
×
514

515
  private boolean runMklink(Path source, Path link, Path cwd, String option) {
516

517
    LOG.trace("Creating a Windows link with mklink {} at {} pointing to {}", option, link, source);
×
518
    ProcessContext pc = this.context.newProcess().executable("cmd").addArgs("/c", "mklink", option);
×
519
    if (cwd != null) {
×
520
      pc.directory(cwd);
×
521
    }
522
    ProcessResult result = pc.addArgs(link.toString(), source.toString()).run(ProcessMode.DEFAULT);
×
523
    try {
524
      result.failOnError();
×
525
      return true;
×
526
    } catch (RuntimeException e) {
×
527
      LOG.debug("mklink {} failed for {} -> {}: {}", option, link, source, e.getMessage());
×
528
      return false;
×
529
    }
530
  }
531

532
  @Override
533
  public void link(Path source, Path link, boolean relative, PathLinkType type) {
534

535
    Path finalLink = link.toAbsolutePath().normalize();
4✔
536
    Path finalSource;
537
    try {
538
      finalSource = adaptPath(source, finalLink, relative);
6✔
539
    } catch (Exception e) {
×
540
      throw new IllegalStateException("Failed to adapt target (" + source + ") for link (" + finalLink + ") and relative (" + relative + ")", e);
×
541
    }
1✔
542
    Path absoluteSource = finalSource.isAbsolute() ? finalSource : finalLink.getParent().resolve(finalSource).normalize();
11✔
543
    String relativeOrAbsolute = relative ? "relative" : "absolute";
6✔
544
    LOG.debug("Creating {} {} at {} pointing to {}", relativeOrAbsolute, type, finalLink, finalSource);
21✔
545
    deleteLinkIfExists(finalLink);
3✔
546
    try {
547
      // Attention: JavaDoc and position of path arguments can be very confusing - see comment in #1736
548
      if (type == PathLinkType.SYMBOLIC_LINK) {
3!
549
        Files.createSymbolicLink(finalLink, finalSource);
7✔
550
      } else if (type == PathLinkType.HARD_LINK) {
×
551
        Files.createLink(finalLink, finalSource);
×
552
      } else {
553
        throw new IllegalStateException("" + type);
×
554
      }
555
    } catch (FileSystemException e) {
×
556
      if (SystemInfoImpl.INSTANCE.isWindows()) {
×
557
        LOG.info(
×
558
            "Due to lack of permissions, Microsoft's mklink with junction had to be used to create a Symlink. See\n"
559
                + "https://github.com/devonfw/IDEasy/blob/main/documentation/symlink.adoc for further details. Error was: "
560
                + e.getMessage());
×
561
        mklinkOnWindows(finalSource, absoluteSource, finalLink, type, relative);
×
562
      } else {
563
        throw new RuntimeException(e);
×
564
      }
565
    } catch (IOException e) {
×
566
      throw new IllegalStateException("Failed to create a " + relativeOrAbsolute + " " + type + " at " + finalLink + " pointing to " + source, e);
×
567
    }
1✔
568
  }
1✔
569

570
  @Override
571
  public Path getPathStart(Path path, int nameEnd) {
572

573
    int count = path.getNameCount();
3✔
574
    int delta = count - nameEnd - 1;
6✔
575
    if (delta < 0) {
2!
576
      throw new IllegalArgumentException("Cannot get start before segment " + nameEnd + " from path " + path + " that has only " + count + " segments.");
×
577
    }
578
    Path result = path;
2✔
579
    while (delta > 0) {
2✔
580
      result = result.getParent();
3✔
581
      delta--;
2✔
582
    }
583
    return result;
2✔
584
  }
585

586
  @Override
587
  public Path getPathEnd(Path path, int nameStart) {
588

589
    int count = path.getNameCount();
3✔
590
    if (nameStart > count) {
3!
591
      throw new IllegalArgumentException("Cannot get end after segment " + nameStart + " from path " + path + " that has only " + count + " segments.");
×
592
    } else if (nameStart == count) {
3!
593
      return Path.of(".");
×
594
    }
595
    Path result = path.getName(nameStart++);
5✔
596
    while (nameStart < count) {
3✔
597
      result = result.resolve(path.getName(nameStart++));
8✔
598
    }
599
    return result;
2✔
600
  }
601

602
  @Override
603
  public int getCommonNameCount(Path path1, Path path2) {
604

605
    if ((path1 == null) || (path2 == null) || !Objects.equals(path1.getRoot(), path2.getRoot())) {
10✔
606
      return -1;
2✔
607
    }
608
    int minNameCount = Math.min(path1.getNameCount(), path2.getNameCount());
6✔
609
    int i = 0;
2✔
610
    while (i < minNameCount) {
3!
611
      Path segment1 = path1.getName(i);
4✔
612
      Path segment2 = path2.getName(i);
4✔
613
      if (!segment1.equals(segment2)) {
4✔
614
        break;
1✔
615
      }
616
      i++;
1✔
617
    }
1✔
618
    return i;
2✔
619
  }
620

621
  @Override
622
  public Path toRealPath(Path path) {
623

624
    return toRealPath(path, true);
5✔
625
  }
626

627
  @Override
628
  public Path toCanonicalPath(Path path) {
629

630
    return toRealPath(path, false);
5✔
631
  }
632

633
  private Path toRealPath(Path path, boolean resolveLinks) {
634

635
    try {
636
      Path realPath;
637
      if (resolveLinks) {
2✔
638
        realPath = path.toRealPath();
6✔
639
      } else {
640
        realPath = path.toRealPath(LinkOption.NOFOLLOW_LINKS);
9✔
641
      }
642
      if (!realPath.equals(path)) {
4✔
643
        LOG.trace("Resolved path {} to {}", path, realPath);
5✔
644
      }
645
      return realPath;
2✔
646
    } catch (IOException e) {
×
647
      throw new IllegalStateException("Failed to get real path for " + path, e);
×
648
    }
649
  }
650

651
  @Override
652
  public Path createTempDir(String name) {
653

654
    try {
655
      Path tmp = this.context.getTempPath();
4✔
656
      Path tempDir = tmp.resolve(name);
4✔
657
      int tries = 1;
2✔
658
      while (Files.exists(tempDir)) {
5!
659
        long id = System.nanoTime() & 0xFFFF;
×
660
        tempDir = tmp.resolve(name + "-" + id);
×
661
        tries++;
×
662
        if (tries > 200) {
×
663
          throw new IOException("Unable to create unique name!");
×
664
        }
665
      }
×
666
      return Files.createDirectory(tempDir);
5✔
667
    } catch (IOException e) {
×
668
      throw new IllegalStateException("Failed to create temporary directory with prefix '" + name + "'!", e);
×
669
    }
670
  }
671

672
  @Override
673
  public void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtractHook, boolean extract) {
674

675
    if (Files.isDirectory(archiveFile)) {
5✔
676
      LOG.warn("Found directory for download at {} hence copying without extraction!", archiveFile);
4✔
677
      copy(archiveFile, targetDir, FileCopyMode.COPY_TREE_CONTENT);
5✔
678
      postExtractHook(postExtractHook, targetDir);
4✔
679
      return;
1✔
680
    } else if (!extract) {
2✔
681
      mkdirs(targetDir);
3✔
682
      move(archiveFile, targetDir.resolve(archiveFile.getFileName()));
9✔
683
      return;
1✔
684
    }
685
    Path tmpDir = createTempDir("extract-" + archiveFile.getFileName());
7✔
686
    LOG.trace("Trying to extract the file {} to {} and move it to {}.", archiveFile, tmpDir, targetDir);
17✔
687
    String filename = archiveFile.getFileName().toString();
4✔
688
    TarCompression tarCompression = TarCompression.of(filename);
3✔
689
    if (tarCompression != null) {
2✔
690
      extractTar(archiveFile, tmpDir, tarCompression);
6✔
691
    } else {
692
      String extension = FilenameUtil.getExtension(filename);
3✔
693
      if (extension == null) {
2!
694
        throw new IllegalStateException("Unknown archive format without extension - can not extract " + archiveFile);
×
695
      } else {
696
        LOG.trace("Determined file extension {}", extension);
4✔
697
      }
698
      switch (extension) {
8!
699
        case "zip" -> extractZip(archiveFile, tmpDir);
×
700
        case "jar" -> extractJar(archiveFile, tmpDir);
5✔
701
        case "dmg" -> extractDmg(archiveFile, tmpDir);
×
702
        case "msi" -> extractMsi(archiveFile, tmpDir);
×
703
        case "pkg" -> extractPkg(archiveFile, tmpDir);
×
704
        default -> throw new IllegalStateException("Unknown archive format " + extension + ". Can not extract " + archiveFile);
×
705
      }
706
    }
707
    Path properInstallDir = getProperInstallationSubDirOf(tmpDir, archiveFile);
5✔
708
    postExtractHook(postExtractHook, properInstallDir);
4✔
709
    move(properInstallDir, targetDir);
6✔
710
    delete(tmpDir);
3✔
711
  }
1✔
712

713
  private void postExtractHook(Consumer<Path> postExtractHook, Path properInstallDir) {
714

715
    if (postExtractHook != null) {
2✔
716
      postExtractHook.accept(properInstallDir);
3✔
717
    }
718
  }
1✔
719

720
  /**
721
   * @param path the {@link Path} to start the recursive search from.
722
   * @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
723
   *     item in their respective directory and {@code s} is not named "bin".
724
   */
725
  private Path getProperInstallationSubDirOf(Path path, Path archiveFile) {
726

727
    try (Stream<Path> stream = Files.list(path)) {
3✔
728
      Path[] subFiles = stream.toArray(Path[]::new);
8✔
729
      if (subFiles.length == 0) {
3!
730
        throw new CliException("The downloaded package " + archiveFile + " seems to be empty as you can check in the extracted folder " + path);
×
731
      } else if (subFiles.length == 1) {
4✔
732
        String filename = subFiles[0].getFileName().toString();
6✔
733
        if (!filename.equals(IdeContext.FOLDER_BIN) && !filename.equals(IdeContext.FOLDER_CONTENTS) && !filename.endsWith(".app") && Files.isDirectory(
19!
734
            subFiles[0])) {
735
          return getProperInstallationSubDirOf(subFiles[0], archiveFile);
9✔
736
        }
737
      }
738
      return path;
4✔
739
    } catch (IOException e) {
4!
740
      throw new IllegalStateException("Failed to get sub-files of " + path);
×
741
    }
742
  }
743

744
  @Override
745
  public void extractZip(Path file, Path targetDir) {
746

747
    LOG.info("Extracting ZIP file {} to {}", file, targetDir);
5✔
748
    if (this.context.getSystemInfo().isMac()) {
5✔
749
      extractZipWithSystemUnzip(file, targetDir);
5✔
750
    } else {
751
      extractZipWithJava(file, targetDir);
4✔
752
    }
753
  }
1✔
754

755
  private void extractZipWithSystemUnzip(Path file, Path targetDir) {
756

757
    mkdirs(targetDir);
3✔
758
    ProcessContext pc = this.context.newProcess();
4✔
759
    pc.executable("/usr/bin/unzip");
4✔
760
    pc.addArgs("-o", "-q", file, "-d", targetDir);
25✔
761
    pc.run();
3✔
762
  }
1✔
763

764
  private void extractZipWithJava(Path file, Path targetDir) {
765

766
    URI uri = URI.create("jar:" + file.toUri());
6✔
767
    try (FileSystem fs = FileSystems.newFileSystem(uri, FS_ENV)) {
4✔
768
      long size = 0;
2✔
769
      for (Path root : fs.getRootDirectories()) {
11✔
770
        size += getFileSizeRecursive(root);
6✔
771
      }
1✔
772
      try (final IdeProgressBar progressBar = this.context.newProgressbarForExtracting(size)) {
5✔
773
        for (Path root : fs.getRootDirectories()) {
11✔
774
          copy(root, targetDir, FileCopyMode.EXTRACT, (s, t, d) -> onFileCopiedFromZip(s, t, d, progressBar));
15✔
775
        }
1✔
776
      }
777
    } catch (IOException e) {
×
778
      throw new IllegalStateException("Failed to extract " + file + " to " + targetDir, e);
×
779
    }
1✔
780
  }
1✔
781

782
  @SuppressWarnings("unchecked")
783
  private void onFileCopiedFromZip(Path source, Path target, boolean directory, IdeProgressBar progressBar) {
784

785
    if (directory) {
2✔
786
      return;
1✔
787
    }
788
    if (!this.context.getSystemInfo().isWindows()) {
5✔
789
      try {
790
        Object attribute = Files.getAttribute(source, "zip:permissions");
6✔
791
        if (attribute instanceof Set<?> permissionSet) {
6✔
792
          Files.setPosixFilePermissions(target, (Set<PosixFilePermission>) permissionSet);
4✔
793
        }
794
      } catch (Exception e) {
×
795
        LOG.error("Failed to transfer zip permissions for {}", target, e);
×
796
      }
1✔
797
    }
798
    progressBar.stepBy(getFileSize(target));
5✔
799
  }
1✔
800

801
  @Override
802
  public void extractTar(Path file, Path targetDir, TarCompression compression) {
803

804
    extractArchive(file, targetDir, in -> new TarArchiveInputStream(compression.unpack(in)));
13✔
805
  }
1✔
806

807
  @Override
808
  public void extractJar(Path file, Path targetDir) {
809

810
    extractZip(file, targetDir);
4✔
811
  }
1✔
812

813
  /**
814
   * @param permissions The integer as returned by {@link TarArchiveEntry#getMode()} that represents the file permissions of a file on a Unix file system.
815
   * @return A String representing the file permissions. E.g. "rwxrwxr-x" or "rw-rw-r--"
816
   */
817
  public static String generatePermissionString(int permissions) {
818

819
    // Ensure that only the last 9 bits are considered
820
    permissions &= 0b111111111;
4✔
821

822
    StringBuilder permissionStringBuilder = new StringBuilder("rwxrwxrwx");
5✔
823
    for (int i = 0; i < 9; i++) {
7✔
824
      int mask = 1 << i;
4✔
825
      char currentChar = ((permissions & mask) != 0) ? permissionStringBuilder.charAt(8 - i) : '-';
12✔
826
      permissionStringBuilder.setCharAt(8 - i, currentChar);
6✔
827
    }
828

829
    return permissionStringBuilder.toString();
3✔
830
  }
831

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

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

836
    final List<PathLink> links = new ArrayList<>();
4✔
837
    try (InputStream is = Files.newInputStream(file);
5✔
838
        ArchiveInputStream<?> ais = unpacker.apply(is);
5✔
839
        IdeProgressBar pb = this.context.newProgressbarForExtracting(getFileSize(file))) {
7✔
840

841
      final Path root = targetDir.toAbsolutePath().normalize();
4✔
842

843
      ArchiveEntry entry = ais.getNextEntry();
3✔
844
      while (entry != null) {
2✔
845
        String entryName = entry.getName();
3✔
846
        Path entryPath = resolveRelativePathSecure(entryName, root);
5✔
847
        PathPermissions permissions = null;
2✔
848
        PathLinkType linkType = null;
2✔
849
        if (entry instanceof TarArchiveEntry tae) {
6!
850
          if (tae.isSymbolicLink()) {
3✔
851
            linkType = PathLinkType.SYMBOLIC_LINK;
3✔
852
          } else if (tae.isLink()) {
3!
853
            linkType = PathLinkType.HARD_LINK;
×
854
          }
855
          if (linkType == null) {
2✔
856
            permissions = PathPermissions.of(tae.getMode());
5✔
857
          } else {
858
            Path parent = entryPath.getParent();
3✔
859
            String sourcePathString = tae.getLinkName();
3✔
860
            Path source = parent.resolve(sourcePathString).normalize();
5✔
861
            source = resolveRelativePathSecure(source, root, sourcePathString);
6✔
862
            links.add(new PathLink(source, entryPath, linkType));
9✔
863
            mkdirs(parent);
3✔
864
          }
865
        }
866
        if (entry.isDirectory()) {
3✔
867
          mkdirs(entryPath);
4✔
868
        } else if (linkType == null) { // regular file
2✔
869
          mkdirs(entryPath.getParent());
4✔
870
          Files.copy(ais, entryPath, StandardCopyOption.REPLACE_EXISTING);
10✔
871
          // POSIX perms on non-Windows
872
          if (permissions != null) {
2!
873
            setFilePermissions(entryPath, permissions, false);
5✔
874
          }
875
        }
876
        pb.stepBy(Math.max(0L, entry.getSize()));
6✔
877
        entry = ais.getNextEntry();
3✔
878
      }
1✔
879
      // post process links
880
      for (PathLink link : links) {
10✔
881
        link(link);
3✔
882
      }
1✔
883
    } catch (Exception e) {
×
884
      throw new IllegalStateException("Failed to extract " + file + " to " + targetDir, e);
×
885
    }
1✔
886
  }
1✔
887

888
  private Path resolveRelativePathSecure(String entryName, Path root) {
889

890
    Path entryPath = root.resolve(entryName).normalize();
5✔
891
    return resolveRelativePathSecure(entryPath, root, entryName);
6✔
892
  }
893

894
  private Path resolveRelativePathSecure(Path entryPath, Path root, String entryName) {
895

896
    if (!entryPath.startsWith(root)) {
4!
897
      throw new IllegalStateException("Preventing path traversal attack from " + entryName + " to " + entryPath + " leaving " + root);
×
898
    }
899
    return entryPath;
2✔
900
  }
901

902
  @Override
903
  public void extractDmg(Path file, Path targetDir) {
904

905
    LOG.info("Extracting DMG file {} to {}", file, targetDir);
×
906
    assert this.context.getSystemInfo().isMac();
×
907

908
    Path mountPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES).resolve(IdeContext.FOLDER_VOLUME);
×
909
    mkdirs(mountPath);
×
910
    ProcessContext pc = this.context.newProcess();
×
911
    pc.executable("hdiutil");
×
912
    pc.addArgs("attach", "-quiet", "-nobrowse", "-mountpoint", mountPath, file);
×
913
    pc.run();
×
914
    Path appPath = findFirst(mountPath, p -> p.getFileName().toString().endsWith(".app"), false);
×
915
    if (appPath == null) {
×
916
      throw new IllegalStateException("Failed to unpack DMG as no MacOS *.app was found in file " + file);
×
917
    }
918

919
    copy(appPath, targetDir, FileCopyMode.COPY_TREE_OVERRIDE_TREE);
×
920
    pc.addArgs("detach", "-force", mountPath);
×
921
    pc.run();
×
922
  }
×
923

924
  @Override
925
  public void extractMsi(Path file, Path targetDir) {
926

927
    LOG.info("Extracting MSI file {} to {}", file, targetDir);
×
928
    this.context.newProcess().executable("msiexec").addArgs("/a", file, "/qn", "TARGETDIR=" + targetDir).run();
×
929
    // msiexec also creates a copy of the MSI
930
    Path msiCopy = targetDir.resolve(file.getFileName());
×
931
    delete(msiCopy);
×
932
  }
×
933

934
  @Override
935
  public void extractPkg(Path file, Path targetDir) {
936

937
    LOG.info("Extracting PKG file {} to {}", file, targetDir);
×
938
    Path tmpDirPkg = createTempDir("ide-pkg-");
×
939
    ProcessContext pc = this.context.newProcess();
×
940
    // we might also be able to use cpio from commons-compression instead of external xar...
941
    pc.executable("xar").addArgs("-C", tmpDirPkg, "-xf", file).run();
×
942
    Path contentPath = findFirst(tmpDirPkg, p -> p.getFileName().toString().equals("Payload"), true);
×
943
    extractTar(contentPath, targetDir, TarCompression.GZ);
×
944
    delete(tmpDirPkg);
×
945
  }
×
946

947
  @Override
948
  public void compress(Path dir, OutputStream out, String path) {
949

950
    String extension = FilenameUtil.getExtension(path);
3✔
951
    TarCompression tarCompression = TarCompression.of(extension);
3✔
952
    if (tarCompression != null) {
2!
953
      compressTar(dir, out, tarCompression);
6✔
954
    } else if (extension.equals("zip")) {
×
955
      compressZip(dir, out);
×
956
    } else {
957
      throw new IllegalArgumentException("Unsupported extension: " + extension);
×
958
    }
959
  }
1✔
960

961
  @Override
962
  public void compressTar(Path dir, OutputStream out, TarCompression tarCompression) {
963

964
    switch (tarCompression) {
8!
965
      case null -> compressTar(dir, out);
×
966
      case NONE -> compressTar(dir, out);
×
967
      case GZ -> compressTarGz(dir, out);
5✔
968
      case BZIP2 -> compressTarBzip2(dir, out);
×
969
      default -> throw new IllegalArgumentException("Unsupported tar compression: " + tarCompression);
×
970
    }
971
  }
1✔
972

973
  @Override
974
  public void compressTarGz(Path dir, OutputStream out) {
975

976
    GzipParameters parameters = new GzipParameters();
4✔
977
    parameters.setModificationTime(0);
3✔
978
    try (GzipCompressorOutputStream gzOut = new GzipCompressorOutputStream(out, parameters)) {
6✔
979
      compressTarOrThrow(dir, gzOut);
4✔
980
    } catch (IOException e) {
×
981
      throw new IllegalStateException("Failed to compress directory " + dir + " to tar.gz file.", e);
×
982
    }
1✔
983
  }
1✔
984

985
  @Override
986
  public void compressTarBzip2(Path dir, OutputStream out) {
987

988
    try (BZip2CompressorOutputStream bzip2Out = new BZip2CompressorOutputStream(out)) {
×
989
      compressTarOrThrow(dir, bzip2Out);
×
990
    } catch (IOException e) {
×
991
      throw new IllegalStateException("Failed to compress directory " + dir + " to tar.bz2 file.", e);
×
992
    }
×
993
  }
×
994

995
  @Override
996
  public void compressTar(Path dir, OutputStream out) {
997

998
    try {
999
      compressTarOrThrow(dir, out);
×
1000
    } catch (IOException e) {
×
1001
      throw new IllegalStateException("Failed to compress directory " + dir + " to tar file.", e);
×
1002
    }
×
1003
  }
×
1004

1005
  private void compressTarOrThrow(Path dir, OutputStream out) throws IOException {
1006

1007
    try (TarArchiveOutputStream tarOut = new TarArchiveOutputStream(out)) {
5✔
1008
      compressRecursive(dir, tarOut, "");
5✔
1009
      tarOut.finish();
2✔
1010
    }
1011
  }
1✔
1012

1013
  @Override
1014
  public void compressZip(Path dir, OutputStream out) {
1015

1016
    try (ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(out)) {
5✔
1017
      compressRecursive(dir, zipOut, "");
5✔
1018
      zipOut.finish();
2✔
1019
    } catch (IOException e) {
×
1020
      throw new IllegalStateException("Failed to compress directory " + dir + " to zip file.", e);
×
1021
    }
1✔
1022
  }
1✔
1023

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

1026
    try (Stream<Path> childStream = Files.list(path).sorted()) {
4✔
1027
      Iterator<Path> iterator = childStream.iterator();
3✔
1028
      while (iterator.hasNext()) {
3✔
1029
        Path child = iterator.next();
4✔
1030
        String relativeChildPath = relativePath + "/" + child.getFileName().toString();
6✔
1031
        boolean isDirectory = Files.isDirectory(child);
5✔
1032
        E archiveEntry = out.createArchiveEntry(child, relativeChildPath);
7✔
1033
        FileTime none = FileTime.fromMillis(0);
3✔
1034
        if (archiveEntry instanceof TarArchiveEntry tarEntry) {
6✔
1035
          tarEntry.setCreationTime(none);
3✔
1036
          tarEntry.setModTime(none);
3✔
1037
          tarEntry.setLastAccessTime(none);
3✔
1038
          tarEntry.setLastModifiedTime(none);
3✔
1039
          tarEntry.setUserId(0);
3✔
1040
          tarEntry.setUserName("user");
3✔
1041
          tarEntry.setGroupId(0);
3✔
1042
          tarEntry.setGroupName("group");
3✔
1043
          PathPermissions filePermissions = getFilePermissions(child);
4✔
1044
          tarEntry.setMode(filePermissions.toMode());
4✔
1045
        } else if (archiveEntry instanceof ZipArchiveEntry zipEntry) {
7!
1046
          zipEntry.setCreationTime(none);
4✔
1047
          zipEntry.setLastAccessTime(none);
4✔
1048
          zipEntry.setLastModifiedTime(none);
4✔
1049
          zipEntry.setTime(none);
3✔
1050
        }
1051
        out.putArchiveEntry(archiveEntry);
3✔
1052
        if (!isDirectory) {
2✔
1053
          try (InputStream in = Files.newInputStream(child)) {
5✔
1054
            IOUtils.copy(in, out);
4✔
1055
          }
1056
        }
1057
        out.closeArchiveEntry();
2✔
1058
        if (isDirectory) {
2✔
1059
          compressRecursive(child, out, relativeChildPath);
5✔
1060
        }
1061
      }
1✔
1062
    } catch (IOException e) {
×
1063
      throw new IllegalStateException("Failed to compress " + path, e);
×
1064
    }
1✔
1065
  }
1✔
1066

1067
  @Override
1068
  public void delete(Path path) {
1069

1070
    if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
9✔
1071
      LOG.trace("Deleting {} skipped as the path does not exist.", path);
4✔
1072
      return;
1✔
1073
    }
1074
    LOG.debug("Deleting {} ...", path);
4✔
1075
    try {
1076
      if (isLink(path)) {
4✔
1077
        deletePath(path);
4✔
1078
      } else {
1079
        deleteRecursive(path);
3✔
1080
      }
1081
    } catch (IOException e) {
×
1082
      throw new IllegalStateException("Failed to delete " + path, e);
×
1083
    }
1✔
1084
  }
1✔
1085

1086
  private void deleteRecursive(Path path) throws IOException {
1087

1088
    if (Files.isSymbolicLink(path) || isJunction(path)) {
7!
1089
      LOG.trace("Deleting link {} ...", path);
4✔
1090
      Files.delete(path);
2✔
1091
      return;
1✔
1092
    }
1093
    if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
9✔
1094
      try (Stream<Path> childStream = Files.list(path)) {
3✔
1095
        Iterator<Path> iterator = childStream.iterator();
3✔
1096
        while (iterator.hasNext()) {
3✔
1097
          Path child = iterator.next();
4✔
1098
          deleteRecursive(child);
3✔
1099
        }
1✔
1100
      }
1101
    }
1102
    deletePath(path);
3✔
1103
  }
1✔
1104

1105
  private boolean isLink(Path path) {
1106

1107
    return Files.isSymbolicLink(path) || isJunction(path);
11!
1108
  }
1109

1110
  private void deletePath(Path path) throws IOException {
1111

1112
    LOG.trace("Deleting {} ...", path);
4✔
1113
    boolean isSetWritable = setWritable(path, true);
5✔
1114
    if (!isSetWritable) {
2✔
1115
      LOG.debug("Couldn't give write access to file: {}", path);
4✔
1116
    }
1117
    Files.delete(path);
2✔
1118
  }
1✔
1119

1120
  @Override
1121
  public Path findFirst(Path dir, Predicate<Path> filter, boolean recursive) {
1122

1123
    try {
1124
      if (!Files.isDirectory(dir)) {
5!
1125
        return null;
×
1126
      }
1127
      return findFirstRecursive(dir, filter, recursive);
6✔
1128
    } catch (IOException e) {
×
1129
      throw new IllegalStateException("Failed to search for file in " + dir, e);
×
1130
    }
1131
  }
1132

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

1135
    List<Path> folders = null;
2✔
1136
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
1137
      Iterator<Path> iterator = childStream.iterator();
3✔
1138
      while (iterator.hasNext()) {
3✔
1139
        Path child = iterator.next();
4✔
1140
        if (filter.test(child)) {
4✔
1141
          return child;
4✔
1142
        } else if (recursive && Files.isDirectory(child)) {
2!
1143
          if (folders == null) {
×
1144
            folders = new ArrayList<>();
×
1145
          }
1146
          folders.add(child);
×
1147
        }
1148
      }
1✔
1149
    }
4!
1150
    if (folders != null) {
2!
1151
      for (Path child : folders) {
×
1152
        Path match = findFirstRecursive(child, filter, recursive);
×
1153
        if (match != null) {
×
1154
          return match;
×
1155
        }
1156
      }
×
1157
    }
1158
    return null;
2✔
1159
  }
1160

1161
  @Override
1162
  public Path findAncestor(Path path, Path baseDir, int subfolderCount) {
1163

1164
    if ((path == null) || (baseDir == null)) {
4!
1165
      LOG.debug("Path should not be null for findAncestor.");
3✔
1166
      return null;
2✔
1167
    }
1168
    if (subfolderCount <= 0) {
2!
1169
      throw new IllegalArgumentException("Subfolder count: " + subfolderCount);
×
1170
    }
1171
    // 1. option relativize
1172
    // 2. recursive getParent
1173
    // 3. loop getParent???
1174
    // 4. getName + getNameCount
1175
    path = path.toAbsolutePath().normalize();
4✔
1176
    baseDir = baseDir.toAbsolutePath().normalize();
4✔
1177
    int directoryNameCount = path.getNameCount();
3✔
1178
    int baseDirNameCount = baseDir.getNameCount();
3✔
1179
    int delta = directoryNameCount - baseDirNameCount - subfolderCount;
6✔
1180
    if (delta < 0) {
2!
1181
      return null;
×
1182
    }
1183
    // ensure directory is a sub-folder of baseDir
1184
    for (int i = 0; i < baseDirNameCount; i++) {
7✔
1185
      if (!path.getName(i).toString().equals(baseDir.getName(i).toString())) {
10✔
1186
        return null;
2✔
1187
      }
1188
    }
1189
    Path result = path;
2✔
1190
    while (delta > 0) {
2✔
1191
      result = result.getParent();
3✔
1192
      delta--;
2✔
1193
    }
1194
    return result;
2✔
1195
  }
1196

1197
  @Override
1198
  public List<Path> listChildrenMapped(Path dir, Function<Path, Path> filter) {
1199

1200
    if (!Files.isDirectory(dir)) {
5✔
1201
      return List.of();
2✔
1202
    }
1203
    List<Path> children = new ArrayList<>();
4✔
1204
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
1205
      Iterator<Path> iterator = childStream.iterator();
3✔
1206
      while (iterator.hasNext()) {
3✔
1207
        Path child = iterator.next();
4✔
1208
        Path filteredChild = filter.apply(child);
5✔
1209
        if (filteredChild != null) {
2✔
1210
          if (filteredChild == child) {
3!
1211
            LOG.trace("Accepted file {}", child);
5✔
1212
          } else {
1213
            LOG.trace("Accepted file {} and mapped to {}", child, filteredChild);
×
1214
          }
1215
          children.add(filteredChild);
5✔
1216
        } else {
1217
          LOG.trace("Ignoring file {} according to filter", child);
4✔
1218
        }
1219
      }
1✔
1220
    } catch (IOException e) {
×
1221
      throw new IllegalStateException("Failed to find children of directory " + dir, e);
×
1222
    }
1✔
1223
    return children;
2✔
1224
  }
1225

1226
  @Override
1227
  public boolean isEmptyDir(Path dir) {
1228

1229
    return listChildren(dir, f -> true).isEmpty();
6✔
1230
  }
1231

1232
  @Override
1233
  public boolean isNonEmptyFile(Path file) {
1234

1235
    if (Files.isRegularFile(file)) {
5✔
1236
      return (getFileSize(file) > 0);
10✔
1237
    }
1238
    return false;
2✔
1239
  }
1240

1241
  private long getFileSize(Path file) {
1242

1243
    try {
1244
      return Files.size(file);
3✔
1245
    } catch (IOException e) {
×
1246
      LOG.warn("Failed to determine size of file {}: {}", file, e.toString(), e);
×
1247
      return 0;
×
1248
    }
1249
  }
1250

1251
  private long getFileSizeRecursive(Path path) {
1252

1253
    long size = 0;
2✔
1254
    if (Files.isDirectory(path)) {
5✔
1255
      try (Stream<Path> childStream = Files.list(path)) {
3✔
1256
        Iterator<Path> iterator = childStream.iterator();
3✔
1257
        while (iterator.hasNext()) {
3✔
1258
          Path child = iterator.next();
4✔
1259
          size += getFileSizeRecursive(child);
6✔
1260
        }
1✔
1261
      } catch (IOException e) {
×
1262
        throw new RuntimeException("Failed to iterate children of folder " + path, e);
×
1263
      }
1✔
1264
    } else {
1265
      size += getFileSize(path);
6✔
1266
    }
1267
    return size;
2✔
1268
  }
1269

1270
  @Override
1271
  public Path findExistingFile(String fileName, List<Path> searchDirs) {
1272

1273
    for (Path dir : searchDirs) {
10!
1274
      Path filePath = dir.resolve(fileName);
4✔
1275
      try {
1276
        if (Files.exists(filePath)) {
5✔
1277
          return filePath;
2✔
1278
        }
1279
      } catch (Exception e) {
×
1280
        throw new IllegalStateException("Unexpected error while checking existence of file " + filePath + " .", e);
×
1281
      }
1✔
1282
    }
1✔
1283
    return null;
×
1284
  }
1285

1286
  @Override
1287
  public boolean setWritable(Path file, boolean writable) {
1288

1289
    if (!Files.exists(file)) {
5✔
1290
      return false;
2✔
1291
    }
1292
    try {
1293
      // POSIX
1294
      PosixFileAttributeView posix = Files.getFileAttributeView(file, PosixFileAttributeView.class);
7✔
1295
      if (posix != null) {
2!
1296
        Set<PosixFilePermission> permissions = new HashSet<>(posix.readAttributes().permissions());
7✔
1297
        boolean changed;
1298
        if (writable) {
2!
1299
          changed = permissions.add(PosixFilePermission.OWNER_WRITE);
5✔
1300
        } else {
1301
          changed = permissions.remove(PosixFilePermission.OWNER_WRITE);
×
1302
        }
1303
        if (changed) {
2!
1304
          posix.setPermissions(permissions);
×
1305
        }
1306
        return true;
2✔
1307
      }
1308

1309
      // Windows
1310
      DosFileAttributeView dos = Files.getFileAttributeView(file, DosFileAttributeView.class);
×
1311
      if (dos != null) {
×
1312
        dos.setReadOnly(!writable);
×
1313
        return true;
×
1314
      }
1315

1316
      LOG.debug("Failed to set writing permission for file {}", file);
×
1317
      return false;
×
1318

1319
    } catch (IOException e) {
×
1320
      LOG.debug("Error occurred when trying to set writing permission for file {}: {}", file, e.toString(), e);
×
1321
      return false;
×
1322
    }
1323
  }
1324

1325
  @Override
1326
  public void makeExecutable(Path path, boolean confirm) {
1327

1328
    if (Files.exists(path)) {
5✔
1329
      if (skipPermissionsIfWindows(path)) {
4!
1330
        return;
×
1331
      }
1332
      PathPermissions existingPermissions = getFilePermissions(path);
4✔
1333
      PathPermissions executablePermissions = existingPermissions.makeExecutable();
3✔
1334
      boolean update = (executablePermissions != existingPermissions);
7✔
1335
      if (update) {
2✔
1336
        if (confirm) {
2!
1337
          boolean yesContinue = this.context.question(
×
1338
              "We want to execute {} but this command seems to lack executable permissions!\n"
1339
                  + "Most probably the tool vendor did forget to add x-flags in the binary release package.\n"
1340
                  + "Before running the command, we suggest to set executable permissions to the file:\n"
1341
                  + "{}\n"
1342
                  + "For security reasons we ask for your confirmation so please check this request.\n"
1343
                  + "Changing permissions from {} to {}.\n"
1344
                  + "Do you confirm to make the command executable before running it?", path.getFileName(), path, existingPermissions, executablePermissions);
×
1345
          if (!yesContinue) {
×
1346
            return;
×
1347
          }
1348
        }
1349
        setFilePermissions(path, executablePermissions, false);
6✔
1350
      } else {
1351
        LOG.trace("Executable flags already present so no need to set them for file {}", path);
4✔
1352
      }
1353
    } else {
1✔
1354
      LOG.warn("Cannot set executable flag on file that does not exist: {}", path);
4✔
1355
    }
1356
  }
1✔
1357

1358
  @Override
1359
  public PathPermissions getFilePermissions(Path path) {
1360

1361
    PathPermissions pathPermissions;
1362
    String info = "";
2✔
1363
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1364
      info = "mocked-";
×
1365
      if (Files.isDirectory(path)) {
×
1366
        pathPermissions = PathPermissions.MODE_RWX_RX_RX;
×
1367
      } else {
1368
        Path parent = path.getParent();
×
1369
        if ((parent != null) && (parent.getFileName().toString().equals("bin"))) {
×
1370
          pathPermissions = PathPermissions.MODE_RWX_RX_RX;
×
1371
        } else {
1372
          pathPermissions = PathPermissions.MODE_RW_R_R;
×
1373
        }
1374
      }
×
1375
    } else {
1376
      Set<PosixFilePermission> permissions;
1377
      try {
1378
        // Read the current file permissions
1379
        permissions = Files.getPosixFilePermissions(path);
5✔
1380
      } catch (IOException e) {
×
1381
        throw new RuntimeException("Failed to get permissions for " + path, e);
×
1382
      }
1✔
1383
      pathPermissions = PathPermissions.of(permissions);
3✔
1384
    }
1385
    LOG.trace("Read {}permissions of {} as {}.", info, path, pathPermissions);
17✔
1386
    return pathPermissions;
2✔
1387
  }
1388

1389
  @Override
1390
  public void setFilePermissions(Path path, PathPermissions permissions, boolean logErrorAndContinue) {
1391

1392
    if (skipPermissionsIfWindows(path)) {
4!
1393
      return;
×
1394
    }
1395
    try {
1396
      LOG.debug("Setting permissions for {} to {}", path, permissions);
5✔
1397
      // Set the new permissions
1398
      Files.setPosixFilePermissions(path, permissions.toPosix());
5✔
1399
    } catch (IOException e) {
×
1400
      String message = "Failed to set permissions to " + permissions + " for path " + path;
×
1401
      if (logErrorAndContinue) {
×
1402
        LOG.warn(message, e);
×
1403
      } else {
1404
        throw new RuntimeException(message, e);
×
1405
      }
1406
    }
1✔
1407
  }
1✔
1408

1409
  private boolean skipPermissionsIfWindows(Path path) {
1410

1411
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1412
      LOG.trace("Windows does not have file permissions hence omitting for {}", path);
×
1413
      return true;
×
1414
    }
1415
    return false;
2✔
1416
  }
1417

1418
  @Override
1419
  public void touch(Path file) {
1420

1421
    if (Files.exists(file)) {
5✔
1422
      try {
1423
        Files.setLastModifiedTime(file, FileTime.fromMillis(System.currentTimeMillis()));
5✔
1424
      } catch (IOException e) {
×
1425
        throw new IllegalStateException("Could not update modification-time of " + file, e);
×
1426
      }
1✔
1427
    } else {
1428
      try {
1429
        Files.createFile(file);
5✔
1430
      } catch (IOException e) {
1✔
1431
        throw new IllegalStateException("Could not create empty file " + file, e);
8✔
1432
      }
1✔
1433
    }
1434
  }
1✔
1435

1436
  @Override
1437
  public String readFileContent(Path file) {
1438

1439
    LOG.trace("Reading content of file from {}", file);
4✔
1440
    if (!Files.exists((file))) {
5✔
1441
      LOG.debug("File {} does not exist", file);
4✔
1442
      return null;
2✔
1443
    }
1444
    try {
1445
      String content = Files.readString(file);
3✔
1446
      LOG.trace("Completed reading {} character(s) from file {}", content.length(), file);
7✔
1447
      return content;
2✔
1448
    } catch (IOException e) {
×
1449
      throw new IllegalStateException("Failed to read file " + file, e);
×
1450
    }
1451
  }
1452

1453
  @Override
1454
  public void writeFileContent(String content, Path file, boolean createParentDir) {
1455

1456
    if (createParentDir) {
2✔
1457
      mkdirs(file.getParent());
4✔
1458
    }
1459
    if (content == null) {
2!
1460
      content = "";
×
1461
    }
1462
    LOG.trace("Writing content with {} character(s) to file {}", content.length(), file);
7✔
1463
    if (Files.exists(file)) {
5✔
1464
      LOG.info("Overriding content of file {}", file);
4✔
1465
    }
1466
    try {
1467
      Files.writeString(file, content);
6✔
1468
      LOG.trace("Wrote content to file {}", file);
4✔
1469
    } catch (IOException e) {
×
1470
      throw new RuntimeException("Failed to write file " + file, e);
×
1471
    }
1✔
1472
  }
1✔
1473

1474
  @Override
1475
  public List<String> readFileLines(Path file) {
1476

1477
    LOG.trace("Reading lines of file from {}", file);
4✔
1478
    if (!Files.exists(file)) {
5✔
1479
      LOG.warn("File {} does not exist", file);
4✔
1480
      return null;
2✔
1481
    }
1482
    try {
1483
      List<String> content = Files.readAllLines(file);
3✔
1484
      LOG.trace("Completed reading {} lines from file {}", content.size(), file);
7✔
1485
      return content;
2✔
1486
    } catch (IOException e) {
×
1487
      throw new IllegalStateException("Failed to read file " + file, e);
×
1488
    }
1489
  }
1490

1491
  @Override
1492
  public void writeFileLines(List<String> content, Path file, boolean createParentDir) {
1493

1494
    if (createParentDir) {
2!
1495
      mkdirs(file.getParent());
×
1496
    }
1497
    if (content == null) {
2!
1498
      content = List.of();
×
1499
    }
1500
    LOG.trace("Writing content with {} lines to file {}", content.size(), file);
7✔
1501
    if (Files.exists(file)) {
5✔
1502
      LOG.debug("Overriding content of file {}", file);
4✔
1503
    }
1504
    try {
1505
      Files.write(file, content);
6✔
1506
      LOG.trace("Wrote lines to file {}", file);
4✔
1507
    } catch (IOException e) {
×
1508
      throw new RuntimeException("Failed to write file " + file, e);
×
1509
    }
1✔
1510
  }
1✔
1511

1512
  @Override
1513
  public void readProperties(Path file, Properties properties) {
1514

1515
    try (Reader reader = Files.newBufferedReader(file)) {
3✔
1516
      properties.load(reader);
3✔
1517
      LOG.debug("Successfully loaded {} properties from {}", properties.size(), file);
7✔
1518
    } catch (IOException e) {
×
1519
      throw new IllegalStateException("Failed to read properties file: " + file, e);
×
1520
    }
1✔
1521
  }
1✔
1522

1523
  @Override
1524
  public void writeProperties(Properties properties, Path file, boolean createParentDir) {
1525

1526
    if (createParentDir) {
2✔
1527
      mkdirs(file.getParent());
4✔
1528
    }
1529
    try (Writer writer = Files.newBufferedWriter(file)) {
5✔
1530
      properties.store(writer, null); // do not get confused - Java still writes a date/time header that cannot be omitted
4✔
1531
      LOG.debug("Successfully saved {} properties to {}", properties.size(), file);
7✔
1532
    } catch (IOException e) {
×
1533
      throw new IllegalStateException("Failed to save properties file during tests.", e);
×
1534
    }
1✔
1535
  }
1✔
1536

1537
  @Override
1538
  public void readIniFile(Path file, IniFile iniFile) {
1539

1540
    if (!Files.exists(file)) {
5✔
1541
      LOG.debug("INI file {} does not exist.", iniFile);
4✔
1542
      return;
1✔
1543
    }
1544
    List<String> iniLines = readFileLines(file);
4✔
1545
    IniSection currentIniSection = iniFile.getInitialSection();
3✔
1546
    for (String line : iniLines) {
10✔
1547
      if (line.trim().startsWith("[")) {
5✔
1548
        currentIniSection = iniFile.getOrCreateSection(line);
5✔
1549
      } else if (line.isBlank() || IniComment.COMMENT_SYMBOLS.contains(line.trim().charAt(0))) {
11✔
1550
        currentIniSection.addComment(line);
4✔
1551
      } else {
1552
        int index = line.indexOf('=');
4✔
1553
        if (index > 0) {
2!
1554
          currentIniSection.setPropertyLine(line);
3✔
1555
        }
1556
      }
1557
    }
1✔
1558
  }
1✔
1559

1560
  @Override
1561
  public void writeIniFile(IniFile iniFile, Path file, boolean createParentDir) {
1562

1563
    String iniString = iniFile.toString();
3✔
1564
    writeFileContent(iniString, file, createParentDir);
5✔
1565
  }
1✔
1566

1567
  @Override
1568
  public Duration getFileAge(Path path) {
1569

1570
    if (Files.exists(path)) {
5✔
1571
      try {
1572
        long currentTime = System.currentTimeMillis();
2✔
1573
        long fileModifiedTime = Files.getLastModifiedTime(path).toMillis();
6✔
1574
        return Duration.ofMillis(currentTime - fileModifiedTime);
5✔
1575
      } catch (IOException e) {
×
1576
        LOG.warn("Could not get modification-time of {}.", path, e);
×
1577
      }
×
1578
    } else {
1579
      LOG.debug("Path {} is missing - skipping modification-time and file age check.", path);
4✔
1580
    }
1581
    return null;
2✔
1582
  }
1583

1584
  @Override
1585
  public boolean isFileAgeRecent(Path path, Duration cacheDuration) {
1586

1587
    Duration age = getFileAge(path);
4✔
1588
    if (age == null) {
2✔
1589
      return false;
2✔
1590
    }
1591
    LOG.debug("The path {} was last updated {} ago and caching duration is {}.", path, age, cacheDuration);
17✔
1592
    return (age.toMillis() <= cacheDuration.toMillis());
10✔
1593
  }
1594
}
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