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

devonfw / IDEasy / 16754552751

05 Aug 2025 03:29PM UTC coverage: 69.457% (+1.0%) from 68.446%
16754552751

Pull #1418

github

web-flow
Merge 5f6fc3206 into d13807b57
Pull Request #1418: Use WindowsHelper to find bash instead of direct ProcessBuilder

3338 of 5242 branches covered (63.68%)

Branch coverage included in aggregate %.

8617 of 11970 relevant lines covered (71.99%)

3.17 hits per line

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

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

49
import org.apache.commons.compress.archivers.ArchiveEntry;
50
import org.apache.commons.compress.archivers.ArchiveInputStream;
51
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
52
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
53
import org.jline.utils.Log;
54

55
import com.devonfw.tools.ide.cli.CliException;
56
import com.devonfw.tools.ide.cli.CliOfflineException;
57
import com.devonfw.tools.ide.context.IdeContext;
58
import com.devonfw.tools.ide.os.SystemInfoImpl;
59
import com.devonfw.tools.ide.process.ProcessContext;
60
import com.devonfw.tools.ide.util.DateTimeUtil;
61
import com.devonfw.tools.ide.util.FilenameUtil;
62
import com.devonfw.tools.ide.util.HexUtil;
63
import com.devonfw.tools.ide.variable.IdeVariables;
64

65
/**
66
 * Implementation of {@link FileAccess}.
67
 */
68
public class FileAccessImpl implements FileAccess {
69

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

72
  private static final String WINDOWS_FILE_LOCK_WARNING =
73
      "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"
74
          + WINDOWS_FILE_LOCK_DOCUMENTATION_PAGE;
75

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

78
  private final IdeContext context;
79

80
  /**
81
   * The constructor.
82
   *
83
   * @param context the {@link IdeContext} to use.
84
   */
85
  public FileAccessImpl(IdeContext context) {
86

87
    super();
2✔
88
    this.context = context;
3✔
89
  }
1✔
90

91
  private HttpClient createHttpClient(String url) {
92

93
    HttpClient.Builder builder = HttpClient.newBuilder().followRedirects(Redirect.ALWAYS);
4✔
94
    return builder.build();
3✔
95
  }
96

97
  @Override
98
  public void download(String url, Path target) {
99
    List<Version> httpProtocols = IdeVariables.HTTP_VERSIONS.get(context);
6✔
100

101
    Exception lastException = null;
2✔
102
    if (httpProtocols.isEmpty()) {
3!
103
      try {
104
        this.downloadWithHttpVersion(url, target, null);
5✔
105
        return; // success
1✔
106
      } catch (Exception e) {
×
107
        lastException = e;
×
108
      }
×
109
    } else {
110
      for (Version version : httpProtocols) {
×
111
        try {
112
          this.context.debug("Trying to download: {} with HTTP protocol version: {}", url, version);
×
113
          this.downloadWithHttpVersion(url, target, version);
×
114
          return; // success
×
115
        } catch (Exception ex) {
×
116
          lastException = ex;
×
117
        }
118
      }
×
119
    }
120

121
    throw new IllegalStateException("Failed to download file from URL " + url + " to " + target, lastException);
×
122
  }
123

124
  private void downloadWithHttpVersion(String url, Path target, Version httpVersion) throws Exception {
125

126
    this.context.info("Trying to download {} from {}", target.getFileName(), url);
15✔
127
    mkdirs(target.getParent());
4✔
128

129
    if (this.context.isOffline()) {
4!
130
      throw CliOfflineException.ofDownloadViaUrl(url);
×
131
    }
132
    if (url.startsWith("http")) {
4✔
133

134
      Builder builder = HttpRequest.newBuilder()
2✔
135
          .uri(URI.create(url))
2✔
136
          .GET();
2✔
137
      if (httpVersion != null) {
2!
138
        builder.version(httpVersion);
×
139
      }
140
      HttpRequest request = builder.build();
3✔
141
      HttpResponse<InputStream> response;
142
      HttpClient client = createHttpClient(url);
4✔
143
      response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
5✔
144
      int statusCode = response.statusCode();
3✔
145
      if (statusCode == 200) {
3!
146
        downloadFileWithProgressBar(url, target, response);
6✔
147
      } else {
148
        throw new IllegalStateException("Download failed with status code " + statusCode);
×
149
      }
150
    } else if (url.startsWith("ftp") || url.startsWith("sftp")) {
9!
151
      throw new IllegalArgumentException("Unsupported download URL: " + url);
×
152
    } else {
153
      Path source = Path.of(url);
5✔
154
      if (isFile(source)) {
4!
155
        // network drive
156

157
        copyFileWithProgressBar(source, target);
5✔
158
      } else {
159
        throw new IllegalArgumentException("Download path does not point to a downloadable file: " + url);
×
160
      }
161
    }
162
  }
1✔
163

164
  /**
165
   * Downloads a file while showing a {@link IdeProgressBar}.
166
   *
167
   * @param url the url to download.
168
   * @param target Path of the target directory.
169
   * @param response the {@link HttpResponse} to use.
170
   */
171
  private void downloadFileWithProgressBar(String url, Path target, HttpResponse<InputStream> response) {
172

173
    long contentLength = response.headers().firstValueAsLong("content-length").orElse(-1);
7✔
174
    informAboutMissingContentLength(contentLength, url);
4✔
175

176
    byte[] data = new byte[1024];
3✔
177
    boolean fileComplete = false;
2✔
178
    int count;
179

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

194
    } catch (Exception e) {
×
195
      throw new RuntimeException(e);
×
196
    }
1✔
197
  }
1✔
198

199
  /**
200
   * Copies a file while displaying a progress bar.
201
   *
202
   * @param source Path of file to copy.
203
   * @param target Path of target directory.
204
   */
205
  private void copyFileWithProgressBar(Path source, Path target) throws IOException {
206

207
    try (InputStream in = new FileInputStream(source.toFile()); OutputStream out = new FileOutputStream(target.toFile())) {
12✔
208
      long size = getFileSize(source);
4✔
209
      byte[] buf = new byte[1024];
3✔
210
      try (IdeProgressBar pb = this.context.newProgressbarForCopying(size)) {
5✔
211
        int readBytes;
212
        while ((readBytes = in.read(buf)) > 0) {
6✔
213
          out.write(buf, 0, readBytes);
5✔
214
          if (size > 0) {
4!
215
            pb.stepBy(readBytes);
5✔
216
          }
217
        }
218
      } catch (Exception e) {
×
219
        throw new RuntimeException(e);
×
220
      }
1✔
221
    }
222
  }
1✔
223

224
  private void informAboutMissingContentLength(long contentLength, String url) {
225

226
    if (contentLength < 0) {
4✔
227
      this.context.warning("Content-Length was not provided by download from {}", url);
10✔
228
    }
229
  }
1✔
230

231
  @Override
232
  public void mkdirs(Path directory) {
233

234
    if (Files.isDirectory(directory)) {
5✔
235
      return;
1✔
236
    }
237
    this.context.trace("Creating directory {}", directory);
10✔
238
    try {
239
      Files.createDirectories(directory);
5✔
240
    } catch (IOException e) {
×
241
      throw new IllegalStateException("Failed to create directory " + directory, e);
×
242
    }
1✔
243
  }
1✔
244

245
  @Override
246
  public boolean isFile(Path file) {
247

248
    if (!Files.exists(file)) {
5!
249
      this.context.trace("File {} does not exist", file);
×
250
      return false;
×
251
    }
252
    if (Files.isDirectory(file)) {
5!
253
      this.context.trace("Path {} is a directory but a regular file was expected", file);
×
254
      return false;
×
255
    }
256
    return true;
2✔
257
  }
258

259
  @Override
260
  public boolean isExpectedFolder(Path folder) {
261

262
    if (Files.isDirectory(folder)) {
5✔
263
      return true;
2✔
264
    }
265
    this.context.warning("Expected folder was not found at {}", folder);
10✔
266
    return false;
2✔
267
  }
268

269
  @Override
270
  public String checksum(Path file, String hashAlgorithm) {
271

272
    MessageDigest md;
273
    try {
274
      md = MessageDigest.getInstance(hashAlgorithm);
×
275
    } catch (NoSuchAlgorithmException e) {
×
276
      throw new IllegalStateException("No such hash algorithm " + hashAlgorithm, e);
×
277
    }
×
278
    byte[] buffer = new byte[1024];
×
279
    try (InputStream is = Files.newInputStream(file); DigestInputStream dis = new DigestInputStream(is, md)) {
×
280
      int read = 0;
×
281
      while (read >= 0) {
×
282
        read = dis.read(buffer);
×
283
      }
284
    } catch (Exception e) {
×
285
      throw new IllegalStateException("Failed to read and hash file " + file, e);
×
286
    }
×
287
    byte[] digestBytes = md.digest();
×
288
    return HexUtil.toHexString(digestBytes);
×
289
  }
290

291
  public boolean isJunction(Path path) {
292

293
    if (!SystemInfoImpl.INSTANCE.isWindows()) {
3!
294
      return false;
2✔
295
    }
296

297
    try {
298
      BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
×
299
      return attr.isOther() && attr.isDirectory();
×
300
    } catch (NoSuchFileException e) {
×
301
      return false; // file doesn't exist
×
302
    } catch (IOException e) {
×
303
      // errors in reading the attributes of the file
304
      throw new IllegalStateException("An unexpected error occurred whilst checking if the file: " + path + " is a junction", e);
×
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
      LocalDateTime now = LocalDateTime.now();
2✔
315
      String date = DateTimeUtil.formatDate(now, true);
4✔
316
      String time = DateTimeUtil.formatTime(now);
3✔
317
      String filename = fileOrFolder.getFileName().toString();
4✔
318
      Path backupPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_BACKUPS).resolve(date).resolve(time + "_" + filename);
12✔
319
      backupPath = appendParentPath(backupPath, fileOrFolder.getParent(), 2);
6✔
320
      mkdirs(backupPath);
3✔
321
      Path target = backupPath.resolve(filename);
4✔
322
      this.context.info("Creating backup by moving {} to {}", fileOrFolder, target);
14✔
323
      move(fileOrFolder, target);
6✔
324
      return target;
2✔
325
    } else {
326
      this.context.trace("Backup of {} skipped as the path does not exist.", fileOrFolder);
10✔
327
    }
328
    return fileOrFolder;
2✔
329
  }
330

331
  private static Path appendParentPath(Path path, Path parent, int max) {
332

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

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

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

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

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

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

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

424
  /**
425
   * 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
426
   * {@link Path} that is neither a symbolic link nor a Windows junction.
427
   *
428
   * @param path the {@link Path} to delete.
429
   * @throws IOException if the actual {@link Files#delete(Path) deletion} fails.
430
   */
431
  private void deleteLinkIfExists(Path path) throws IOException {
432

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

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

438
    if (isJunction || isSymlink) {
4!
439
      this.context.info("Deleting previous " + (isJunction ? "junction" : "symlink") + " at " + path);
9!
440
      Files.delete(path);
2✔
441
    }
442
  }
1✔
443

444
  /**
445
   * Adapts the given {@link Path} to be relative or absolute depending on the given {@code relative} flag. Additionally, {@link Path#toRealPath(LinkOption...)}
446
   * is applied to {@code source}.
447
   *
448
   * @param source the {@link Path} to adapt.
449
   * @param targetLink the {@link Path} used to calculate the relative path to the {@code source} if {@code relative} is set to {@code true}.
450
   * @param relative the {@code relative} flag.
451
   * @return the adapted {@link Path}.
452
   * @see FileAccessImpl#symlink(Path, Path, boolean)
453
   */
454
  private Path adaptPath(Path source, Path targetLink, boolean relative) throws IOException {
455

456
    if (source.isAbsolute()) {
3✔
457
      try {
458
        source = source.toRealPath(LinkOption.NOFOLLOW_LINKS); // to transform ../d1/../d2 to ../d2
9✔
459
      } catch (IOException e) {
×
460
        throw new IOException("Calling toRealPath() on the source (" + source + ") in method FileAccessImpl.adaptPath() failed.", e);
×
461
      }
1✔
462
      if (relative) {
2✔
463
        source = targetLink.getParent().relativize(source);
5✔
464
        // to make relative links like this work: dir/link -> dir
465
        source = (source.toString().isEmpty()) ? Path.of(".") : source;
12✔
466
      }
467
    } else { // source is relative
468
      if (relative) {
2✔
469
        // even though the source is already relative, toRealPath should be called to transform paths like
470
        // this ../d1/../d2 to ../d2
471
        source = targetLink.getParent().relativize(targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS));
14✔
472
        source = (source.toString().isEmpty()) ? Path.of(".") : source;
12✔
473
      } else { // !relative
474
        try {
475
          source = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS);
11✔
476
        } catch (IOException e) {
×
477
          throw new IOException("Calling toRealPath() on " + targetLink + ".resolveSibling(" + source + ") in method FileAccessImpl.adaptPath() failed.", e);
×
478
        }
1✔
479
      }
480
    }
481
    return source;
2✔
482
  }
483

484
  /**
485
   * Creates a Windows junction at {@code targetLink} pointing to {@code source}.
486
   *
487
   * @param source must be another Windows junction or a directory.
488
   * @param targetLink the location of the Windows junction.
489
   */
490
  private void createWindowsJunction(Path source, Path targetLink) {
491

492
    this.context.trace("Creating a Windows junction at " + targetLink + " with " + source + " as source.");
×
493
    Path fallbackPath;
494
    if (!source.isAbsolute()) {
×
495
      this.context.warning("You are on Windows and you do not have permissions to create symbolic links. Junctions are used as an "
×
496
          + "alternative, however, these can not point to relative paths. So the source (" + source + ") is interpreted as an absolute path.");
497
      try {
498
        fallbackPath = targetLink.resolveSibling(source).toRealPath(LinkOption.NOFOLLOW_LINKS);
×
499
      } catch (IOException e) {
×
500
        throw new IllegalStateException(
×
501
            "Since Windows junctions are used, the source must be an absolute path. The transformation of the passed " + "source (" + source
502
                + ") to an absolute path failed.", e);
503
      }
×
504

505
    } else {
506
      fallbackPath = source;
×
507
    }
508
    if (!Files.isDirectory(fallbackPath)) { // if source is a junction. This returns true as well.
×
509
      throw new IllegalStateException(
×
510
          "These junctions can only point to directories or other junctions. Please make sure that the source (" + fallbackPath + ") is one of these.");
511
    }
512
    this.context.newProcess().executable("cmd").addArgs("/c", "mklink", "/d", "/j", targetLink.toString(), fallbackPath.toString()).run();
×
513
  }
×
514

515
  @Override
516
  public void symlink(Path source, Path targetLink, boolean relative) {
517

518
    Path adaptedSource = null;
2✔
519
    try {
520
      adaptedSource = adaptPath(source, targetLink, relative);
6✔
521
    } catch (IOException e) {
×
522
      throw new IllegalStateException("Failed to adapt source for source (" + source + ") target (" + targetLink + ") and relative (" + relative + ")", e);
×
523
    }
1✔
524
    this.context.debug("Creating {} symbolic link {} pointing to {}", adaptedSource.isAbsolute() ? "" : "relative", targetLink, adaptedSource);
23✔
525

526
    try {
527
      deleteLinkIfExists(targetLink);
3✔
528
    } catch (IOException e) {
×
529
      throw new IllegalStateException("Failed to delete previous symlink or Windows junction at " + targetLink, e);
×
530
    }
1✔
531

532
    try {
533
      Files.createSymbolicLink(targetLink, adaptedSource);
6✔
534
    } catch (FileSystemException e) {
×
535
      if (SystemInfoImpl.INSTANCE.isWindows()) {
×
536
        this.context.info("Due to lack of permissions, Microsoft's mklink with junction had to be used to create "
×
537
            + "a Symlink. See https://github.com/devonfw/IDEasy/blob/main/documentation/symlink.adoc for " + "further details. Error was: "
538
            + e.getMessage());
×
539
        createWindowsJunction(adaptedSource, targetLink);
×
540
      } else {
541
        throw new RuntimeException(e);
×
542
      }
543
    } catch (IOException e) {
×
544
      throw new IllegalStateException(
×
545
          "Failed to create a " + (adaptedSource.isAbsolute() ? "" : "relative") + "symbolic link " + targetLink + " pointing to " + source, e);
×
546
    }
1✔
547
  }
1✔
548

549
  @Override
550
  public Path toRealPath(Path path) {
551

552
    return toRealPath(path, true);
5✔
553
  }
554

555
  @Override
556
  public Path toCanonicalPath(Path path) {
557

558
    return toRealPath(path, false);
5✔
559
  }
560

561
  private Path toRealPath(Path path, boolean resolveLinks) {
562

563
    try {
564
      Path realPath;
565
      if (resolveLinks) {
2✔
566
        realPath = path.toRealPath();
6✔
567
      } else {
568
        realPath = path.toRealPath(LinkOption.NOFOLLOW_LINKS);
9✔
569
      }
570
      if (!realPath.equals(path)) {
4✔
571
        this.context.trace("Resolved path {} to {}", path, realPath);
14✔
572
      }
573
      return realPath;
2✔
574
    } catch (IOException e) {
×
575
      throw new IllegalStateException("Failed to get real path for " + path, e);
×
576
    }
577
  }
578

579
  @Override
580
  public Path createTempDir(String name) {
581

582
    try {
583
      Path tmp = this.context.getTempPath();
4✔
584
      Path tempDir = tmp.resolve(name);
4✔
585
      int tries = 1;
2✔
586
      while (Files.exists(tempDir)) {
5!
587
        long id = System.nanoTime() & 0xFFFF;
×
588
        tempDir = tmp.resolve(name + "-" + id);
×
589
        tries++;
×
590
        if (tries > 200) {
×
591
          throw new IOException("Unable to create unique name!");
×
592
        }
593
      }
×
594
      return Files.createDirectory(tempDir);
5✔
595
    } catch (IOException e) {
×
596
      throw new IllegalStateException("Failed to create temporary directory with prefix '" + name + "'!", e);
×
597
    }
598
  }
599

600
  @Override
601
  public void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtractHook, boolean extract) {
602

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

643
  private void postExtractHook(Consumer<Path> postExtractHook, Path properInstallDir) {
644

645
    if (postExtractHook != null) {
2✔
646
      postExtractHook.accept(properInstallDir);
3✔
647
    }
648
  }
1✔
649

650
  /**
651
   * @param path the {@link Path} to start the recursive search from.
652
   * @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
653
   *     item in their respective directory and {@code s} is not named "bin".
654
   */
655
  private Path getProperInstallationSubDirOf(Path path, Path archiveFile) {
656

657
    try (Stream<Path> stream = Files.list(path)) {
3✔
658
      Path[] subFiles = stream.toArray(Path[]::new);
8✔
659
      if (subFiles.length == 0) {
3!
660
        throw new CliException("The downloaded package " + archiveFile + " seems to be empty as you can check in the extracted folder " + path);
×
661
      } else if (subFiles.length == 1) {
4✔
662
        String filename = subFiles[0].getFileName().toString();
6✔
663
        if (!filename.equals(IdeContext.FOLDER_BIN) && !filename.equals(IdeContext.FOLDER_CONTENTS) && !filename.endsWith(".app") && Files.isDirectory(
19!
664
            subFiles[0])) {
665
          return getProperInstallationSubDirOf(subFiles[0], archiveFile);
9✔
666
        }
667
      }
668
      return path;
4✔
669
    } catch (IOException e) {
4!
670
      throw new IllegalStateException("Failed to get sub-files of " + path);
×
671
    }
672
  }
673

674
  @Override
675
  public void extractZip(Path file, Path targetDir) {
676

677
    this.context.info("Extracting ZIP file {} to {}", file, targetDir);
14✔
678
    URI uri = URI.create("jar:" + file.toUri());
6✔
679
    try (FileSystem fs = FileSystems.newFileSystem(uri, FS_ENV)) {
4✔
680
      long size = 0;
2✔
681
      for (Path root : fs.getRootDirectories()) {
11✔
682
        size += getFileSizeRecursive(root);
6✔
683
      }
1✔
684
      try (final IdeProgressBar progressBar = this.context.newProgressbarForExtracting(size)) {
5✔
685
        for (Path root : fs.getRootDirectories()) {
11✔
686
          copy(root, targetDir, FileCopyMode.EXTRACT, (s, t, d) -> onFileCopiedFromZip(s, t, d, progressBar));
15✔
687
        }
1✔
688
      }
689
    } catch (IOException e) {
×
690
      throw new IllegalStateException("Failed to extract " + file + " to " + targetDir, e);
×
691
    }
1✔
692
  }
1✔
693

694
  @SuppressWarnings("unchecked")
695
  private void onFileCopiedFromZip(Path source, Path target, boolean directory, IdeProgressBar progressBar) {
696

697
    if (directory) {
2✔
698
      return;
1✔
699
    }
700
    if (!context.getSystemInfo().isWindows()) {
5✔
701
      try {
702
        Object attribute = Files.getAttribute(source, "zip:permissions");
6✔
703
        if (attribute instanceof Set<?> permissionSet) {
6✔
704
          Files.setPosixFilePermissions(target, (Set<PosixFilePermission>) permissionSet);
4✔
705
        }
706
      } catch (Exception e) {
×
707
        context.error(e, "Failed to transfer zip permissions for {}", target);
×
708
      }
1✔
709
    }
710
    progressBar.stepBy(getFileSize(target));
5✔
711
  }
1✔
712

713
  @Override
714
  public void extractTar(Path file, Path targetDir, TarCompression compression) {
715

716
    extractArchive(file, targetDir, in -> new TarArchiveInputStream(compression.unpack(in)));
13✔
717
  }
1✔
718

719
  @Override
720
  public void extractJar(Path file, Path targetDir) {
721

722
    extractZip(file, targetDir);
4✔
723
  }
1✔
724

725
  /**
726
   * @param permissions The integer as returned by {@link TarArchiveEntry#getMode()} that represents the file permissions of a file on a Unix file system.
727
   * @return A String representing the file permissions. E.g. "rwxrwxr-x" or "rw-rw-r--"
728
   */
729
  public static String generatePermissionString(int permissions) {
730

731
    // Ensure that only the last 9 bits are considered
732
    permissions &= 0b111111111;
4✔
733

734
    StringBuilder permissionStringBuilder = new StringBuilder("rwxrwxrwx");
5✔
735
    for (int i = 0; i < 9; i++) {
7✔
736
      int mask = 1 << i;
4✔
737
      char currentChar = ((permissions & mask) != 0) ? permissionStringBuilder.charAt(8 - i) : '-';
12✔
738
      permissionStringBuilder.setCharAt(8 - i, currentChar);
6✔
739
    }
740

741
    return permissionStringBuilder.toString();
3✔
742
  }
743

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

746
    this.context.info("Extracting TAR file {} to {}", file, targetDir);
14✔
747
    try (InputStream is = Files.newInputStream(file);
5✔
748
        ArchiveInputStream<?> ais = unpacker.apply(is);
5✔
749
        IdeProgressBar pb = this.context.newProgressbarForExtracting(getFileSize(file))) {
7✔
750

751
      ArchiveEntry entry = ais.getNextEntry();
3✔
752
      boolean isTar = ais instanceof TarArchiveInputStream;
3✔
753
      while (entry != null) {
2✔
754
        String permissionStr = null;
2✔
755
        if (isTar) {
2!
756
          int tarMode = ((TarArchiveEntry) entry).getMode();
4✔
757
          permissionStr = generatePermissionString(tarMode);
3✔
758
        }
759
        Path entryName = Path.of(entry.getName());
6✔
760
        Path entryPath = targetDir.resolve(entryName).toAbsolutePath();
5✔
761
        if (!entryPath.startsWith(targetDir)) {
4!
762
          throw new IOException("Preventing path traversal attack from " + entryName + " to " + entryPath);
×
763
        }
764
        if (entry.isDirectory()) {
3✔
765
          mkdirs(entryPath);
4✔
766
        } else {
767
          // ensure the file can also be created if directory entry was missing or out of order...
768
          mkdirs(entryPath.getParent());
4✔
769
          Files.copy(ais, entryPath);
6✔
770
        }
771
        if (isTar && !this.context.getSystemInfo().isWindows()) {
7!
772
          Set<PosixFilePermission> permissions = PosixFilePermissions.fromString(permissionStr);
3✔
773
          Files.setPosixFilePermissions(entryPath, permissions);
4✔
774
        }
775
        pb.stepBy(entry.getSize());
4✔
776
        entry = ais.getNextEntry();
3✔
777
      }
1✔
778
    } catch (IOException e) {
×
779
      throw new IllegalStateException("Failed to extract " + file + " to " + targetDir, e);
×
780
    }
1✔
781
  }
1✔
782

783
  @Override
784
  public void extractDmg(Path file, Path targetDir) {
785

786
    this.context.info("Extracting DMG file {} to {}", file, targetDir);
×
787
    assert this.context.getSystemInfo().isMac();
×
788

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

800
    copy(appPath, targetDir, FileCopyMode.COPY_TREE_OVERRIDE_TREE);
×
801
    pc.addArgs("detach", "-force", mountPath);
×
802
    pc.run();
×
803
  }
×
804

805
  @Override
806
  public void extractMsi(Path file, Path targetDir) {
807

808
    this.context.info("Extracting MSI file {} to {}", file, targetDir);
×
809
    this.context.newProcess().executable("msiexec").addArgs("/a", file, "/qn", "TARGETDIR=" + targetDir).run();
×
810
    // msiexec also creates a copy of the MSI
811
    Path msiCopy = targetDir.resolve(file.getFileName());
×
812
    delete(msiCopy);
×
813
  }
×
814

815
  @Override
816
  public void extractPkg(Path file, Path targetDir) {
817

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

828
  @Override
829
  public void delete(Path path) {
830

831
    if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
9✔
832
      this.context.trace("Deleting {} skipped as the path does not exist.", path);
10✔
833
      return;
1✔
834
    }
835
    this.context.debug("Deleting {} ...", path);
10✔
836
    try {
837
      if (Files.isSymbolicLink(path) || isJunction(path)) {
7!
838
        Files.delete(path);
3✔
839
      } else {
840
        deleteRecursive(path);
3✔
841
      }
842
    } catch (IOException e) {
×
843
      throw new IllegalStateException("Failed to delete " + path, e);
×
844
    }
1✔
845
  }
1✔
846

847
  private void deleteRecursive(Path path) throws IOException {
848

849
    if (Files.isDirectory(path)) {
5✔
850
      try (Stream<Path> childStream = Files.list(path)) {
3✔
851
        Iterator<Path> iterator = childStream.iterator();
3✔
852
        while (iterator.hasNext()) {
3✔
853
          Path child = iterator.next();
4✔
854
          deleteRecursive(child);
3✔
855
        }
1✔
856
      }
857
    }
858
    this.context.trace("Deleting {} ...", path);
10✔
859
    boolean isSetWritable = setWritable(path, true);
5✔
860
    if (!isSetWritable) {
2✔
861
      this.context.debug("Couldn't give write access to file: " + path);
6✔
862
    }
863
    Files.delete(path);
2✔
864
  }
1✔
865

866
  @Override
867
  public Path findFirst(Path dir, Predicate<Path> filter, boolean recursive) {
868

869
    try {
870
      if (!Files.isDirectory(dir)) {
5✔
871
        return null;
2✔
872
      }
873
      return findFirstRecursive(dir, filter, recursive);
6✔
874
    } catch (IOException e) {
×
875
      throw new IllegalStateException("Failed to search for file in " + dir, e);
×
876
    }
877
  }
878

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

881
    List<Path> folders = null;
2✔
882
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
883
      Iterator<Path> iterator = childStream.iterator();
3✔
884
      while (iterator.hasNext()) {
3✔
885
        Path child = iterator.next();
4✔
886
        if (filter.test(child)) {
4✔
887
          return child;
4✔
888
        } else if (recursive && Files.isDirectory(child)) {
2!
889
          if (folders == null) {
×
890
            folders = new ArrayList<>();
×
891
          }
892
          folders.add(child);
×
893
        }
894
      }
1✔
895
    }
4!
896
    if (folders != null) {
2!
897
      for (Path child : folders) {
×
898
        Path match = findFirstRecursive(child, filter, recursive);
×
899
        if (match != null) {
×
900
          return match;
×
901
        }
902
      }
×
903
    }
904
    return null;
2✔
905
  }
906

907
  @Override
908
  public List<Path> listChildrenMapped(Path dir, Function<Path, Path> filter) {
909

910
    if (!Files.isDirectory(dir)) {
5✔
911
      return List.of();
2✔
912
    }
913
    List<Path> children = new ArrayList<>();
4✔
914
    try (Stream<Path> childStream = Files.list(dir)) {
3✔
915
      Iterator<Path> iterator = childStream.iterator();
3✔
916
      while (iterator.hasNext()) {
3✔
917
        Path child = iterator.next();
4✔
918
        Path filteredChild = filter.apply(child);
5✔
919
        if (filteredChild != null) {
2✔
920
          if (filteredChild == child) {
3!
921
            this.context.trace("Accepted file {}", child);
11✔
922
          } else {
923
            this.context.trace("Accepted file {} and mapped to {}", child, filteredChild);
×
924
          }
925
          children.add(filteredChild);
5✔
926
        } else {
927
          this.context.trace("Ignoring file {} according to filter", child);
10✔
928
        }
929
      }
1✔
930
    } catch (IOException e) {
×
931
      throw new IllegalStateException("Failed to find children of directory " + dir, e);
×
932
    }
1✔
933
    return children;
2✔
934
  }
935

936
  @Override
937
  public boolean isEmptyDir(Path dir) {
938

939
    return listChildren(dir, f -> true).isEmpty();
8✔
940
  }
941

942
  private long getFileSize(Path file) {
943

944
    try {
945
      return Files.size(file);
3✔
946
    } catch (IOException e) {
×
947
      this.context.warning(e.getMessage(), e);
×
948
      return 0;
×
949
    }
950
  }
951

952
  private long getFileSizeRecursive(Path path) {
953

954
    long size = 0;
2✔
955
    if (Files.isDirectory(path)) {
5✔
956
      try (Stream<Path> childStream = Files.list(path)) {
3✔
957
        Iterator<Path> iterator = childStream.iterator();
3✔
958
        while (iterator.hasNext()) {
3✔
959
          Path child = iterator.next();
4✔
960
          size += getFileSizeRecursive(child);
6✔
961
        }
1✔
962
      } catch (IOException e) {
×
963
        throw new RuntimeException("Failed to iterate children of folder " + path, e);
×
964
      }
1✔
965
    } else {
966
      size += getFileSize(path);
6✔
967
    }
968
    return size;
2✔
969
  }
970

971
  @Override
972
  public Path findExistingFile(String fileName, List<Path> searchDirs) {
973

974
    for (Path dir : searchDirs) {
10!
975
      Path filePath = dir.resolve(fileName);
4✔
976
      try {
977
        if (Files.exists(filePath)) {
5✔
978
          return filePath;
2✔
979
        }
980
      } catch (Exception e) {
×
981
        throw new IllegalStateException("Unexpected error while checking existence of file " + filePath + " .", e);
×
982
      }
1✔
983
    }
1✔
984
    return null;
×
985
  }
986

987
  @Override
988
  public boolean setWritable(Path file, boolean writable) {
989
    try {
990
      // POSIX
991
      PosixFileAttributeView posix = Files.getFileAttributeView(file, PosixFileAttributeView.class);
7✔
992
      if (posix != null) {
2!
993
        Set<PosixFilePermission> permissions = new HashSet<>(posix.readAttributes().permissions());
7✔
994
        boolean changed;
995
        if (writable) {
2!
996
          changed = permissions.add(PosixFilePermission.OWNER_WRITE);
5✔
997
        } else {
998
          changed = permissions.remove(PosixFilePermission.OWNER_WRITE);
×
999
        }
1000
        if (changed) {
2!
1001
          posix.setPermissions(permissions);
×
1002
        }
1003
        return true;
2✔
1004
      }
1005

1006
      // Windows
1007
      DosFileAttributeView dos = Files.getFileAttributeView(file, DosFileAttributeView.class);
×
1008
      if (dos != null) {
×
1009
        dos.setReadOnly(!writable);
×
1010
        return true;
×
1011
      }
1012

1013
      this.context.debug("Failed to set writing permission for file {}", file);
×
1014
      return false;
×
1015

1016
    } catch (IOException e) {
1✔
1017
      this.context.debug("Error occurred when trying to set writing permission for file " + file + ": " + e);
8✔
1018
      return false;
2✔
1019
    }
1020
  }
1021

1022
  @Override
1023
  public void makeExecutable(Path file, boolean confirm) {
1024

1025
    if (Files.exists(file)) {
5✔
1026
      if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1027
        this.context.trace("Windows does not have executable flags hence omitting for file {}", file);
×
1028
        return;
×
1029
      }
1030
      try {
1031
        // Read the current file permissions
1032
        Set<PosixFilePermission> existingPermissions = Files.getPosixFilePermissions(file);
5✔
1033

1034
        // Add execute permission for all users
1035
        Set<PosixFilePermission> executablePermissions = new HashSet<>(existingPermissions);
5✔
1036
        boolean update = false;
2✔
1037
        update |= executablePermissions.add(PosixFilePermission.OWNER_EXECUTE);
6✔
1038
        update |= executablePermissions.add(PosixFilePermission.GROUP_EXECUTE);
6✔
1039
        update |= executablePermissions.add(PosixFilePermission.OTHERS_EXECUTE);
6✔
1040

1041
        if (update) {
2✔
1042
          if (confirm) {
2!
1043
            boolean yesContinue = this.context.question(
×
1044
                "We want to execute " + file.getFileName() + " but this command seems to lack executable permissions!\n"
×
1045
                    + "Most probably the tool vendor did forgot to add x-flags in the binary release package.\n"
1046
                    + "Before running the command, we suggest to set executable permissions to the file:\n"
1047
                    + file + "\n"
1048
                    + "For security reasons we ask for your confirmation so please check this request.\n"
1049
                    + "Changing permissions from " + PosixFilePermissions.toString(existingPermissions) + " to " + PosixFilePermissions.toString(
×
1050
                    executablePermissions) + ".\n"
1051
                    + "Do you confirm to make the command executable before running it?");
1052
            if (!yesContinue) {
×
1053
              return;
×
1054
            }
1055
          }
1056
          this.context.debug("Setting executable flags for file {}", file);
10✔
1057
          // Set the new permissions
1058
          Files.setPosixFilePermissions(file, executablePermissions);
5✔
1059
        } else {
1060
          this.context.trace("Executable flags already present so no need to set them for file {}", file);
10✔
1061
        }
1062
      } catch (IOException e) {
×
1063
        throw new RuntimeException(e);
×
1064
      }
1✔
1065
    } else {
1066
      this.context.warning("Cannot set executable flag on file that does not exist: {}", file);
10✔
1067
    }
1068
  }
1✔
1069

1070
  @Override
1071
  public void touch(Path file) {
1072

1073
    if (Files.exists(file)) {
5✔
1074
      try {
1075
        Files.setLastModifiedTime(file, FileTime.fromMillis(System.currentTimeMillis()));
5✔
1076
      } catch (IOException e) {
×
1077
        throw new IllegalStateException("Could not update modification-time of " + file, e);
×
1078
      }
1✔
1079
    } else {
1080
      try {
1081
        Files.createFile(file);
5✔
1082
      } catch (IOException e) {
1✔
1083
        throw new IllegalStateException("Could not create empty file " + file, e);
8✔
1084
      }
1✔
1085
    }
1086
  }
1✔
1087

1088
  @Override
1089
  public String readFileContent(Path file) {
1090

1091
    this.context.trace("Reading content of file from {}", file);
10✔
1092
    if (!Files.exists((file))) {
5!
1093
      this.context.debug("File {} does not exist", file);
×
1094
      return null;
×
1095
    }
1096
    try {
1097
      String content = Files.readString(file);
3✔
1098
      this.context.trace("Completed reading {} character(s) from file {}", content.length(), file);
16✔
1099
      return content;
2✔
1100
    } catch (IOException e) {
×
1101
      throw new IllegalStateException("Failed to read file " + file, e);
×
1102
    }
1103
  }
1104

1105
  @Override
1106
  public void writeFileContent(String content, Path file, boolean createParentDir) {
1107

1108
    if (createParentDir) {
2✔
1109
      mkdirs(file.getParent());
4✔
1110
    }
1111
    if (content == null) {
2!
1112
      content = "";
×
1113
    }
1114
    this.context.trace("Writing content with {} character(s) to file {}", content.length(), file);
16✔
1115
    if (Files.exists(file)) {
5✔
1116
      this.context.info("Overriding content of file {}", file);
10✔
1117
    }
1118
    try {
1119
      Files.writeString(file, content);
6✔
1120
      this.context.trace("Wrote content to file {}", file);
10✔
1121
    } catch (IOException e) {
×
1122
      throw new RuntimeException("Failed to write file " + file, e);
×
1123
    }
1✔
1124
  }
1✔
1125

1126
  @Override
1127
  public List<String> readFileLines(Path file) {
1128

1129
    this.context.trace("Reading content of file from {}", file);
10✔
1130
    if (!Files.exists(file)) {
5✔
1131
      this.context.warning("File {} does not exist", file);
10✔
1132
      return null;
2✔
1133
    }
1134
    try {
1135
      List<String> content = Files.readAllLines(file);
3✔
1136
      this.context.trace("Completed reading {} lines from file {}", content.size(), file);
16✔
1137
      return content;
2✔
1138
    } catch (IOException e) {
×
1139
      throw new IllegalStateException("Failed to read file " + file, e);
×
1140
    }
1141
  }
1142

1143
  @Override
1144
  public void writeFileLines(List<String> content, Path file, boolean createParentDir) {
1145

1146
    if (createParentDir) {
2!
1147
      mkdirs(file.getParent());
×
1148
    }
1149
    if (content == null) {
2!
1150
      content = List.of();
×
1151
    }
1152
    this.context.trace("Writing content with {} lines to file {}", content.size(), file);
16✔
1153
    if (Files.exists(file)) {
5✔
1154
      this.context.debug("Overriding content of file {}", file);
10✔
1155
    }
1156
    try {
1157
      Files.write(file, content);
6✔
1158
      this.context.trace("Wrote content to file {}", file);
10✔
1159
    } catch (IOException e) {
×
1160
      throw new RuntimeException("Failed to write file " + file, e);
×
1161
    }
1✔
1162
  }
1✔
1163

1164
  @Override
1165
  public void readProperties(Path file, Properties properties) {
1166

1167
    try (Reader reader = Files.newBufferedReader(file)) {
3✔
1168
      properties.load(reader);
3✔
1169
      this.context.debug("Successfully loaded {} properties from {}", properties.size(), file);
16✔
1170
    } catch (IOException e) {
×
1171
      throw new IllegalStateException("Failed to read properties file: " + file, e);
×
1172
    }
1✔
1173
  }
1✔
1174

1175
  @Override
1176
  public void writeProperties(Properties properties, Path file, boolean createParentDir) {
1177

1178
    if (createParentDir) {
2✔
1179
      mkdirs(file.getParent());
4✔
1180
    }
1181
    try (Writer writer = Files.newBufferedWriter(file)) {
5✔
1182
      properties.store(writer, null); // do not get confused - Java still writes a date/time header that cannot be omitted
4✔
1183
      this.context.debug("Successfully saved {} properties to {}", properties.size(), file);
16✔
1184
    } catch (IOException e) {
×
1185
      throw new IllegalStateException("Failed to save properties file during tests.", e);
×
1186
    }
1✔
1187
  }
1✔
1188

1189
  @Override
1190
  public void readIniFile(Path file, IniFile iniFile) {
1191
    if (!Files.exists(file)) {
5!
1192
      this.context.debug("INI file {} does not exist.", iniFile);
×
1193
      return;
×
1194
    }
1195
    List<String> iniLines = readFileLines(file);
4✔
1196
    IniSection currentIniSection = null;
2✔
1197
    for (String line : iniLines) {
10✔
1198
      if (line.isEmpty()) {
3!
1199
        continue;
×
1200
      }
1201
      if (line.startsWith("[")) {
4✔
1202
        String sectionName = line.replace("[", "").replace("]", "").trim();
9✔
1203
        currentIniSection = iniFile.getOrCreateSection(sectionName);
4✔
1204
      } else {
1✔
1205
        int index = line.indexOf('=');
4✔
1206
        if (index > 0) {
2!
1207
          String propertyName = line.substring(0, index).trim();
6✔
1208
          String propertyValue = line.substring(index + 1).trim();
7✔
1209
          if (currentIniSection == null) {
2!
1210
            Log.warn("Invalid ini-file with property {} before section", propertyName);
×
1211
          } else {
1212
            currentIniSection.getProperties().put(propertyName, propertyValue);
6✔
1213
          }
1214
        } else {
1215
          // here we can handle comments and empty lines in the future
1216
        }
1217
      }
1218
    }
1✔
1219
  }
1✔
1220

1221
  @Override
1222
  public void writeIniFile(IniFile iniFile, Path file, boolean createParentDir) {
1223
    String iniString = iniFile.toString();
3✔
1224
    writeFileContent(iniString, file, createParentDir);
5✔
1225
  }
1✔
1226

1227
  @Override
1228
  public Duration getFileAge(Path path) {
1229
    if (Files.exists(path)) {
5✔
1230
      try {
1231
        long currentTime = System.currentTimeMillis();
2✔
1232
        long fileModifiedTime = Files.getLastModifiedTime(path).toMillis();
6✔
1233
        return Duration.ofMillis(currentTime - fileModifiedTime);
5✔
1234
      } catch (IOException e) {
×
1235
        this.context.warning().log(e, "Could not get modification-time of {}.", path);
×
1236
      }
×
1237
    } else {
1238
      this.context.debug("Path {} is missing - skipping modification-time and file age check.", path);
10✔
1239
    }
1240
    return null;
2✔
1241
  }
1242

1243
  @Override
1244
  public boolean isFileAgeRecent(Path path, Duration cacheDuration) {
1245

1246
    Duration age = getFileAge(path);
4✔
1247
    if (age == null) {
2✔
1248
      return false;
2✔
1249
    }
1250
    context.debug("The path {} was last updated {} ago and caching duration is {}.", path, age, cacheDuration);
18✔
1251
    return (age.toMillis() <= cacheDuration.toMillis());
10✔
1252
  }
1253
}
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