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

devonfw / IDEasy / 17890947969

21 Sep 2025 07:58AM UTC coverage: 68.447% (-0.03%) from 68.475%
17890947969

push

github

web-flow
#1500: improve mklink fallback (#1502)

3430 of 5489 branches covered (62.49%)

Branch coverage included in aggregate %.

8976 of 12636 relevant lines covered (71.04%)

3.12 hits per line

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

65.71
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.cli.CliOfflineException;
56
import com.devonfw.tools.ide.context.IdeContext;
57
import com.devonfw.tools.ide.io.ini.IniComment;
58
import com.devonfw.tools.ide.io.ini.IniFile;
59
import com.devonfw.tools.ide.io.ini.IniSection;
60
import com.devonfw.tools.ide.os.SystemInfoImpl;
61
import com.devonfw.tools.ide.process.ProcessContext;
62
import com.devonfw.tools.ide.process.ProcessMode;
63
import com.devonfw.tools.ide.process.ProcessResult;
64
import com.devonfw.tools.ide.util.DateTimeUtil;
65
import com.devonfw.tools.ide.util.FilenameUtil;
66
import com.devonfw.tools.ide.util.HexUtil;
67
import com.devonfw.tools.ide.variable.IdeVariables;
68

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

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

76
  private static final String WINDOWS_FILE_LOCK_WARNING = "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 int MODE_RWX_RX_RX = 0755;
80

81
  private static final int MODE_RW_R_R = 0644;
82

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

85
  private final IdeContext context;
86

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

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

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

101
    if (this.context.isOffline()) {
4!
102
      throw CliOfflineException.ofDownloadViaUrl(url);
×
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
      this.context.info("Trying to download {} from {}", target.getFileName(), url);
16✔
147
    } else {
148
      this.context.info("Trying to download: {} with HTTP protocol version: {}", url, httpVersion);
×
149
    }
150
    mkdirs(target.getParent());
4✔
151
    httpGet(url, httpVersion, (response) -> downloadFileWithProgressBar(url, target, response));
13✔
152
  }
1✔
153

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

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

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

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

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

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

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

216
    this.context.debug("Downloading text body from {}", url);
10✔
217
    return httpGetAsString(url);
3✔
218
  }
219

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

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

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

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

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

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

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

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

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

283
    if (!SystemInfoImpl.INSTANCE.isWindows()) {
3!
284
      return false;
2✔
285
    }
286
    try {
287
      BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
×
288
      return attr.isOther() && attr.isDirectory();
×
289
    } catch (NoSuchFileException e) {
×
290
      return false; // file doesn't exist
×
291
    } catch (IOException e) {
×
292
      // errors in reading the attributes of the file
293
      throw new IllegalStateException("An unexpected error occurred whilst checking if the file: " + path + " is a junction", e);
×
294
    }
295
  }
296

297
  @Override
298
  public Path backup(Path fileOrFolder) {
299

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

320
  private static Path appendParentPath(Path path, Path parent, int max) {
321

322
    if ((parent == null) || (max <= 0)) {
4!
323
      return path;
2✔
324
    }
325
    return appendParentPath(path, parent.getParent(), max - 1).resolve(parent.getFileName());
11✔
326
  }
327

328
  @Override
329
  public void move(Path source, Path targetDir, StandardCopyOption... copyOptions) {
330

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

344
  @Override
345
  public void copy(Path source, Path target, FileCopyMode mode, PathCopyListener listener) {
346

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

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

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

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

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

424
    assert !(isSymlink && isJunction);
5!
425

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

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

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

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

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

489
  @Override
490
  public void link(Path target, Path link, boolean relative, PathLinkType type) {
491

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

524
  @Override
525
  public Path toRealPath(Path path) {
526

527
    return toRealPath(path, true);
5✔
528
  }
529

530
  @Override
531
  public Path toCanonicalPath(Path path) {
532

533
    return toRealPath(path, false);
5✔
534
  }
535

536
  private Path toRealPath(Path path, boolean resolveLinks) {
537

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

554
  @Override
555
  public Path createTempDir(String name) {
556

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

575
  @Override
576
  public void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtractHook, boolean extract) {
577

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

616
  private void postExtractHook(Consumer<Path> postExtractHook, Path properInstallDir) {
617

618
    if (postExtractHook != null) {
2✔
619
      postExtractHook.accept(properInstallDir);
3✔
620
    }
621
  }
1✔
622

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

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

647
  @Override
648
  public void extractZip(Path file, Path targetDir) {
649

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

667
  @SuppressWarnings("unchecked")
668
  private void onFileCopiedFromZip(Path source, Path target, boolean directory, IdeProgressBar progressBar) {
669

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

686
  @Override
687
  public void extractTar(Path file, Path targetDir, TarCompression compression) {
688

689
    extractArchive(file, targetDir, in -> new TarArchiveInputStream(compression.unpack(in)));
13✔
690
  }
1✔
691

692
  @Override
693
  public void extractJar(Path file, Path targetDir) {
694

695
    extractZip(file, targetDir);
4✔
696
  }
1✔
697

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

704
    // Ensure that only the last 9 bits are considered
705
    permissions &= 0b111111111;
4✔
706

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

714
    return permissionStringBuilder.toString();
3✔
715
  }
716

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

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

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

726
      final boolean isTar = (ais instanceof TarArchiveInputStream);
3✔
727
      final boolean isWindows = this.context.getSystemInfo().isWindows();
5✔
728
      final Path root = targetDir.toAbsolutePath().normalize();
4✔
729

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

775
  private Path resolveRelativePathSecure(String entryName, Path root) {
776

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

781
  private Path resolveRelativePathSecure(Path entryPath, Path root, String entryName) {
782

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

789
  @Override
790
  public void extractDmg(Path file, Path targetDir) {
791

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

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

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

811
  @Override
812
  public void extractMsi(Path file, Path targetDir) {
813

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

821
  @Override
822
  public void extractPkg(Path file, Path targetDir) {
823

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

834
  @Override
835
  public void compress(Path dir, OutputStream out, String format) {
836

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

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

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

860
  @Override
861
  public void compressTarGz(Path dir, OutputStream out) {
862

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

870
  @Override
871
  public void compressTarBzip2(Path dir, OutputStream out) {
872

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

880
  @Override
881
  public void compressTar(Path dir, OutputStream out) {
882

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

890
  private void compressTarOrThrow(Path dir, OutputStream out) throws IOException {
891

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

898
  @Override
899
  public void compressZip(Path dir, OutputStream out) {
900

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

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

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

954
  @Override
955
  public void delete(Path path) {
956

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

973
  private void deleteRecursive(Path path) throws IOException {
974

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

992
  @Override
993
  public Path findFirst(Path dir, Predicate<Path> filter, boolean recursive) {
994

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

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

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

1033
  @Override
1034
  public List<Path> listChildrenMapped(Path dir, Function<Path, Path> filter) {
1035

1036
    if (!Files.isDirectory(dir)) {
5✔
1037
      return List.of();
2✔
1038
    }
1039
    List<Path> children = new ArrayList<>();
4✔
1040
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
1041
      Iterator<Path> iterator = childStream.iterator();
3✔
1042
      while (iterator.hasNext()) {
3✔
1043
        Path child = iterator.next();
4✔
1044
        Path filteredChild = filter.apply(child);
5✔
1045
        if (filteredChild != null) {
2✔
1046
          if (filteredChild == child) {
3!
1047
            this.context.trace("Accepted file {}", child);
11✔
1048
          } else {
1049
            this.context.trace("Accepted file {} and mapped to {}", child, filteredChild);
×
1050
          }
1051
          children.add(filteredChild);
5✔
1052
        } else {
1053
          this.context.trace("Ignoring file {} according to filter", child);
10✔
1054
        }
1055
      }
1✔
1056
    } catch (IOException e) {
×
1057
      throw new IllegalStateException("Failed to find children of directory " + dir, e);
×
1058
    }
1✔
1059
    return children;
2✔
1060
  }
1061

1062
  @Override
1063
  public boolean isEmptyDir(Path dir) {
1064

1065
    return listChildren(dir, f -> true).isEmpty();
8✔
1066
  }
1067

1068
  private long getFileSize(Path file) {
1069

1070
    try {
1071
      return Files.size(file);
3✔
1072
    } catch (IOException e) {
×
1073
      this.context.warning(e.getMessage(), e);
×
1074
      return 0;
×
1075
    }
1076
  }
1077

1078
  private long getFileSizeRecursive(Path path) {
1079

1080
    long size = 0;
2✔
1081
    if (Files.isDirectory(path)) {
5✔
1082
      try (Stream<Path> childStream = Files.list(path)) {
3✔
1083
        Iterator<Path> iterator = childStream.iterator();
3✔
1084
        while (iterator.hasNext()) {
3✔
1085
          Path child = iterator.next();
4✔
1086
          size += getFileSizeRecursive(child);
6✔
1087
        }
1✔
1088
      } catch (IOException e) {
×
1089
        throw new RuntimeException("Failed to iterate children of folder " + path, e);
×
1090
      }
1✔
1091
    } else {
1092
      size += getFileSize(path);
6✔
1093
    }
1094
    return size;
2✔
1095
  }
1096

1097
  @Override
1098
  public Path findExistingFile(String fileName, List<Path> searchDirs) {
1099

1100
    for (Path dir : searchDirs) {
10!
1101
      Path filePath = dir.resolve(fileName);
4✔
1102
      try {
1103
        if (Files.exists(filePath)) {
5✔
1104
          return filePath;
2✔
1105
        }
1106
      } catch (Exception e) {
×
1107
        throw new IllegalStateException("Unexpected error while checking existence of file " + filePath + " .", e);
×
1108
      }
1✔
1109
    }
1✔
1110
    return null;
×
1111
  }
1112

1113
  @Override
1114
  public boolean setWritable(Path file, boolean writable) {
1115

1116
    try {
1117
      // POSIX
1118
      PosixFileAttributeView posix = Files.getFileAttributeView(file, PosixFileAttributeView.class);
7✔
1119
      if (posix != null) {
2!
1120
        Set<PosixFilePermission> permissions = new HashSet<>(posix.readAttributes().permissions());
7✔
1121
        boolean changed;
1122
        if (writable) {
2!
1123
          changed = permissions.add(PosixFilePermission.OWNER_WRITE);
5✔
1124
        } else {
1125
          changed = permissions.remove(PosixFilePermission.OWNER_WRITE);
×
1126
        }
1127
        if (changed) {
2!
1128
          posix.setPermissions(permissions);
×
1129
        }
1130
        return true;
2✔
1131
      }
1132

1133
      // Windows
1134
      DosFileAttributeView dos = Files.getFileAttributeView(file, DosFileAttributeView.class);
×
1135
      if (dos != null) {
×
1136
        dos.setReadOnly(!writable);
×
1137
        return true;
×
1138
      }
1139

1140
      this.context.debug("Failed to set writing permission for file {}", file);
×
1141
      return false;
×
1142

1143
    } catch (IOException e) {
1✔
1144
      this.context.debug("Error occurred when trying to set writing permission for file " + file + ": " + e);
8✔
1145
      return false;
2✔
1146
    }
1147
  }
1148

1149
  @Override
1150
  public void makeExecutable(Path path, boolean confirm) {
1151

1152
    if (Files.exists(path)) {
5✔
1153
      if (skipPermissionsIfWindows(path)) {
4!
1154
        return;
×
1155
      }
1156
      Set<PosixFilePermission> existingPosixPermissions;
1157
      try {
1158
        // Read the current file permissions
1159
        existingPosixPermissions = Files.getPosixFilePermissions(path);
5✔
1160
      } catch (IOException e) {
×
1161
        throw new RuntimeException("Failed to get permissions for " + path, e);
×
1162
      }
1✔
1163

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

1190
  @Override
1191
  public void setFilePermissions(Path path, PathPermissions permissions, boolean logErrorAndContinue) {
1192

1193
    if (skipPermissionsIfWindows(path)) {
4!
1194
      return;
×
1195
    }
1196
    try {
1197
      this.context.debug("Setting permissions for {} to {}", path, permissions);
14✔
1198
      // Set the new permissions
1199
      Files.setPosixFilePermissions(path, permissions.toPosix());
5✔
1200
    } catch (IOException e) {
×
1201
      if (logErrorAndContinue) {
×
1202
        this.context.warning().log(e, "Failed to set permissions to {} for path {}", permissions, path);
×
1203
      } else {
1204
        throw new RuntimeException("Failed to set permissions to " + permissions + " for path " + path, e);
×
1205
      }
1206
    }
1✔
1207
  }
1✔
1208

1209
  private boolean skipPermissionsIfWindows(Path path) {
1210

1211
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1212
      this.context.trace("Windows does not have file permissions hence omitting for {}", path);
×
1213
      return true;
×
1214
    }
1215
    return false;
2✔
1216
  }
1217

1218
  @Override
1219
  public void touch(Path file) {
1220

1221
    if (Files.exists(file)) {
5✔
1222
      try {
1223
        Files.setLastModifiedTime(file, FileTime.fromMillis(System.currentTimeMillis()));
5✔
1224
      } catch (IOException e) {
×
1225
        throw new IllegalStateException("Could not update modification-time of " + file, e);
×
1226
      }
1✔
1227
    } else {
1228
      try {
1229
        Files.createFile(file);
5✔
1230
      } catch (IOException e) {
1✔
1231
        throw new IllegalStateException("Could not create empty file " + file, e);
8✔
1232
      }
1✔
1233
    }
1234
  }
1✔
1235

1236
  @Override
1237
  public String readFileContent(Path file) {
1238

1239
    this.context.trace("Reading content of file from {}", file);
10✔
1240
    if (!Files.exists((file))) {
5!
1241
      this.context.debug("File {} does not exist", file);
×
1242
      return null;
×
1243
    }
1244
    try {
1245
      String content = Files.readString(file);
3✔
1246
      this.context.trace("Completed reading {} character(s) from file {}", content.length(), file);
16✔
1247
      return content;
2✔
1248
    } catch (IOException e) {
×
1249
      throw new IllegalStateException("Failed to read file " + file, e);
×
1250
    }
1251
  }
1252

1253
  @Override
1254
  public void writeFileContent(String content, Path file, boolean createParentDir) {
1255

1256
    if (createParentDir) {
2✔
1257
      mkdirs(file.getParent());
4✔
1258
    }
1259
    if (content == null) {
2!
1260
      content = "";
×
1261
    }
1262
    this.context.trace("Writing content with {} character(s) to file {}", content.length(), file);
16✔
1263
    if (Files.exists(file)) {
5✔
1264
      this.context.info("Overriding content of file {}", file);
10✔
1265
    }
1266
    try {
1267
      Files.writeString(file, content);
6✔
1268
      this.context.trace("Wrote content to file {}", file);
10✔
1269
    } catch (IOException e) {
×
1270
      throw new RuntimeException("Failed to write file " + file, e);
×
1271
    }
1✔
1272
  }
1✔
1273

1274
  @Override
1275
  public List<String> readFileLines(Path file) {
1276

1277
    this.context.trace("Reading content of file from {}", file);
10✔
1278
    if (!Files.exists(file)) {
5✔
1279
      this.context.warning("File {} does not exist", file);
10✔
1280
      return null;
2✔
1281
    }
1282
    try {
1283
      List<String> content = Files.readAllLines(file);
3✔
1284
      this.context.trace("Completed reading {} lines from file {}", content.size(), file);
16✔
1285
      return content;
2✔
1286
    } catch (IOException e) {
×
1287
      throw new IllegalStateException("Failed to read file " + file, e);
×
1288
    }
1289
  }
1290

1291
  @Override
1292
  public void writeFileLines(List<String> content, Path file, boolean createParentDir) {
1293

1294
    if (createParentDir) {
2!
1295
      mkdirs(file.getParent());
×
1296
    }
1297
    if (content == null) {
2!
1298
      content = List.of();
×
1299
    }
1300
    this.context.trace("Writing content with {} lines to file {}", content.size(), file);
16✔
1301
    if (Files.exists(file)) {
5✔
1302
      this.context.debug("Overriding content of file {}", file);
10✔
1303
    }
1304
    try {
1305
      Files.write(file, content);
6✔
1306
      this.context.trace("Wrote content to file {}", file);
10✔
1307
    } catch (IOException e) {
×
1308
      throw new RuntimeException("Failed to write file " + file, e);
×
1309
    }
1✔
1310
  }
1✔
1311

1312
  @Override
1313
  public void readProperties(Path file, Properties properties) {
1314

1315
    try (Reader reader = Files.newBufferedReader(file)) {
3✔
1316
      properties.load(reader);
3✔
1317
      this.context.debug("Successfully loaded {} properties from {}", properties.size(), file);
16✔
1318
    } catch (IOException e) {
×
1319
      throw new IllegalStateException("Failed to read properties file: " + file, e);
×
1320
    }
1✔
1321
  }
1✔
1322

1323
  @Override
1324
  public void writeProperties(Properties properties, Path file, boolean createParentDir) {
1325

1326
    if (createParentDir) {
2✔
1327
      mkdirs(file.getParent());
4✔
1328
    }
1329
    try (Writer writer = Files.newBufferedWriter(file)) {
5✔
1330
      properties.store(writer, null); // do not get confused - Java still writes a date/time header that cannot be omitted
4✔
1331
      this.context.debug("Successfully saved {} properties to {}", properties.size(), file);
16✔
1332
    } catch (IOException e) {
×
1333
      throw new IllegalStateException("Failed to save properties file during tests.", e);
×
1334
    }
1✔
1335
  }
1✔
1336

1337
  @Override
1338
  public void readIniFile(Path file, IniFile iniFile) {
1339

1340
    if (!Files.exists(file)) {
5!
1341
      this.context.debug("INI file {} does not exist.", iniFile);
×
1342
      return;
×
1343
    }
1344
    List<String> iniLines = readFileLines(file);
4✔
1345
    IniSection currentIniSection = iniFile.getInitialSection();
3✔
1346
    for (String line : iniLines) {
10✔
1347
      if (line.trim().startsWith("[")) {
5✔
1348
        currentIniSection = iniFile.getOrCreateSection(line);
5✔
1349
      } else if (line.isBlank() || IniComment.COMMENT_SYMBOLS.contains(line.trim().charAt(0))) {
11✔
1350
        currentIniSection.addComment(line);
4✔
1351
      } else {
1352
        int index = line.indexOf('=');
4✔
1353
        if (index > 0) {
2!
1354
          currentIniSection.setProperty(line);
3✔
1355
        }
1356
      }
1357
    }
1✔
1358
  }
1✔
1359

1360
  @Override
1361
  public void writeIniFile(IniFile iniFile, Path file, boolean createParentDir) {
1362

1363
    String iniString = iniFile.toString();
3✔
1364
    writeFileContent(iniString, file, createParentDir);
5✔
1365
  }
1✔
1366

1367
  @Override
1368
  public Duration getFileAge(Path path) {
1369

1370
    if (Files.exists(path)) {
5✔
1371
      try {
1372
        long currentTime = System.currentTimeMillis();
2✔
1373
        long fileModifiedTime = Files.getLastModifiedTime(path).toMillis();
6✔
1374
        return Duration.ofMillis(currentTime - fileModifiedTime);
5✔
1375
      } catch (IOException e) {
×
1376
        this.context.warning().log(e, "Could not get modification-time of {}.", path);
×
1377
      }
×
1378
    } else {
1379
      this.context.debug("Path {} is missing - skipping modification-time and file age check.", path);
10✔
1380
    }
1381
    return null;
2✔
1382
  }
1383

1384
  @Override
1385
  public boolean isFileAgeRecent(Path path, Duration cacheDuration) {
1386

1387
    Duration age = getFileAge(path);
4✔
1388
    if (age == null) {
2✔
1389
      return false;
2✔
1390
    }
1391
    this.context.debug("The path {} was last updated {} ago and caching duration is {}.", path, age, cacheDuration);
18✔
1392
    return (age.toMillis() <= cacheDuration.toMillis());
10✔
1393
  }
1394
}
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