Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE

Release with new features and bugfixes:

* https://github.com/devonfw/IDEasy/issues/1439[#1439]: Add deterministic archive generation and checksum verification tests
Comment thread
ducminh02 marked this conversation as resolved.
Outdated
* https://github.com/devonfw/IDEasy/issues/1552[#1552]: Add Commandlet to fix TLS issue
* https://github.com/devonfw/IDEasy/issues/1799[#1799]: Add support for file URL in GitUrl validation for local development
* https://github.com/devonfw/IDEasy/issues/1760[#1760]: Accept empty input for single option
Expand Down
15 changes: 12 additions & 3 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipParameters;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -953,7 +955,9 @@ public void compressTar(Path dir, OutputStream out, TarCompression tarCompressio
@Override
public void compressTarGz(Path dir, OutputStream out) {

try (GzipCompressorOutputStream gzOut = new GzipCompressorOutputStream(out)) {
GzipParameters parameters = new GzipParameters();
parameters.setModificationTime(0);
try (GzipCompressorOutputStream gzOut = new GzipCompressorOutputStream(out, parameters)) {
compressTarOrThrow(dir, gzOut);
} catch (IOException e) {
throw new IllegalStateException("Failed to compress directory " + dir + " to tar.gz file.", e);
Expand Down Expand Up @@ -1001,15 +1005,15 @@ public void compressZip(Path dir, OutputStream out) {

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

try (Stream<Path> childStream = Files.list(path)) {
try (Stream<Path> childStream = Files.list(path).sorted()) {
Iterator<Path> iterator = childStream.iterator();
while (iterator.hasNext()) {
Path child = iterator.next();
String relativeChildPath = relativePath + "/" + child.getFileName().toString();
boolean isDirectory = Files.isDirectory(child);
E archiveEntry = out.createArchiveEntry(child, relativeChildPath);
FileTime none = FileTime.fromMillis(0);
if (archiveEntry instanceof TarArchiveEntry tarEntry) {
FileTime none = FileTime.fromMillis(0);
tarEntry.setCreationTime(none);
tarEntry.setModTime(none);
tarEntry.setLastAccessTime(none);
Expand All @@ -1020,6 +1024,11 @@ private <E extends ArchiveEntry> void compressRecursive(Path path, ArchiveOutput
tarEntry.setGroupName("group");
PathPermissions filePermissions = getFilePermissions(child);
tarEntry.setMode(filePermissions.toMode());
} else if (archiveEntry instanceof ZipArchiveEntry zipEntry) {
zipEntry.setCreationTime(none);
zipEntry.setLastAccessTime(none);
zipEntry.setLastModifiedTime(none);
zipEntry.setTime(none);
Comment thread
hohwille marked this conversation as resolved.
}
out.putArchiveEntry(archiveEntry);
if (!isDirectory) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.devonfw.tools.ide.io;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import com.devonfw.tools.ide.context.AbstractIdeContextTest;
import com.devonfw.tools.ide.context.IdeTestContext;

/**
* Test of archive determinism in {@link FileAccessImpl}.
*/
public class ArchiveDeterminismTest extends AbstractIdeContextTest {

@TempDir
Path tempDir;

/**
* Test that {@link FileAccessImpl#compressTarGz(Path, OutputStream)} is deterministic.
*
* @throws IOException if an I/O error occurs.
*/
@Test
public void testTarGzDeterminism() throws IOException, InterruptedException {

// arrange
IdeTestContext context = new IdeTestContext();
FileAccessImpl fileAccess = new FileAccessImpl(context);
Path contentDir = this.tempDir.resolve("content");
Files.createDirectories(contentDir);
Files.writeString(contentDir.resolve("file1.txt"), "Content 1");
Path binDir = contentDir.resolve("bin");
Files.createDirectories(binDir);
Files.writeString(binDir.resolve("script.sh"), "#!/bin/bash\necho hello");

Path archive1 = this.tempDir.resolve("archive1.tar.gz");
Path archive2 = this.tempDir.resolve("archive2.tar.gz");

// act
try (OutputStream out1 = Files.newOutputStream(archive1)) {
fileAccess.compressTarGz(contentDir, out1);
}
// Wait a bit to ensure a non-deterministic MTIME would change (though we zero it out)
Thread.sleep(1100);
Comment thread
hohwille marked this conversation as resolved.
Outdated
try (OutputStream out2 = Files.newOutputStream(archive2)) {
fileAccess.compressTarGz(contentDir, out2);
}

// assert
assertThat(archive1).hasSameBinaryContentAs(archive2);
}

/**
* Test that {@link FileAccessImpl#compressZip(Path, OutputStream)} is deterministic.
*
* @throws IOException if an I/O error occurs.
*/
@Test
public void testZipDeterminism() throws IOException, InterruptedException {

// arrange
IdeTestContext context = new IdeTestContext();
FileAccessImpl fileAccess = new FileAccessImpl(context);
Path contentDir = this.tempDir.resolve("content-zip");
Files.createDirectories(contentDir);
Files.writeString(contentDir.resolve("file1.txt"), "Content 1");

Path archive1 = this.tempDir.resolve("archive1.zip");
Path archive2 = this.tempDir.resolve("archive2.zip");

// act
try (OutputStream out1 = Files.newOutputStream(archive1)) {
fileAccess.compressZip(contentDir, out1);
}
// Wait a bit to ensure a non-deterministic time would change
Thread.sleep(1100);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as in the tarGz test

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as in the tarGz test

try (OutputStream out2 = Files.newOutputStream(archive2)) {
fileAccess.compressZip(contentDir, out2);
}

// assert
assertThat(archive1).hasSameBinaryContentAs(archive2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.devonfw.tools.ide.tool.repository;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import com.devonfw.tools.ide.cli.CliException;
import com.devonfw.tools.ide.context.AbstractIdeContextTest;
import com.devonfw.tools.ide.context.IdeTestContext;
import com.devonfw.tools.ide.url.model.file.UrlGenericChecksum;

/**
* Test of checksum verification in {@link AbstractToolRepository}.
*/
public class ChecksumVerificationTest extends AbstractIdeContextTest {

@TempDir
Path tempDir;

/**
* Test {@link AbstractToolRepository#verifyChecksum(Path, UrlGenericChecksum)} with matching checksum.
*
* @throws IOException if an I/O error occurs.
*/
@Test
public void testVerifyChecksumMatching() throws IOException {

// arrange
IdeTestContext context = newContext(PROJECT_BASIC);
AbstractToolRepository repo = new DefaultToolRepository(context);
Path file = this.tempDir.resolve("testfile.txt");
String content = "Hello World";
Files.writeString(file, content);
String checksum = context.getFileAccess().checksum(file, "SHA-256");
UrlGenericChecksum expectedChecksum = new TestUrlGenericChecksum(checksum, "SHA-256");

// act
repo.verifyChecksum(file, expectedChecksum);

// assert (no exception thrown)
Comment thread
ducminh02 marked this conversation as resolved.
Outdated
}

/**
* Test {@link AbstractToolRepository#verifyChecksum(Path, UrlGenericChecksum)} with mismatching checksum.
*
* @throws IOException if an I/O error occurs.
*/
@Test
public void testVerifyChecksumMismatch() throws IOException {

// arrange
IdeTestContext context = newContext(PROJECT_BASIC);
AbstractToolRepository repo = new DefaultToolRepository(context);
Path file = this.tempDir.resolve("testfile.txt");
String content = "Hello World";
Files.writeString(file, content);
String wrongChecksum = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; // SHA-256 of empty string
UrlGenericChecksum expectedChecksum = new TestUrlGenericChecksum(wrongChecksum, "SHA-256");

// act & assert
try {
repo.verifyChecksum(file, expectedChecksum);
fail("Exception expected");
Comment thread
ducminh02 marked this conversation as resolved.
Outdated
} catch (CliException e) {
assertThat(e).hasMessageContaining("has the wrong SHA-256 checksum");
assertThat(e).hasMessageContaining("Expected " + wrongChecksum);
}
}

private static class TestUrlGenericChecksum implements UrlGenericChecksum {

private final String checksum;

private final String algorithm;

public TestUrlGenericChecksum(String checksum, String algorithm) {

this.checksum = checksum;
this.algorithm = algorithm;
}

@Override
public String getChecksum() {

return this.checksum;
}

@Override
public String getHashAlgorithm() {

return this.algorithm;
}

@Override
public String toString() {

return this.checksum;
}
}
}
Loading