ArchiveTest.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.junrar;
import com.github.junrar.exception.CrcErrorException;
import com.github.junrar.exception.RarException;
import com.github.junrar.exception.UnsupportedRarV5Exception;
import com.github.junrar.rarfile.FileHeader;
import com.github.junrar.rarfile.HostSystem;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junitpioneer.jupiter.DefaultTimeZone;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.stream.Collectors;
import static java.util.Calendar.FEBRUARY;
import static java.util.Calendar.MARCH;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
public class ArchiveTest {
@Test
public void testTikaDocs() throws Exception {
String[] expected = {"testEXCEL.xls", "13824",
"testHTML.html", "167",
"testOpenOffice2.odt", "26448",
"testPDF.pdf", "34824",
"testPPT.ppt", "16384",
"testRTF.rtf", "3410",
"testTXT.txt", "49",
"testWORD.doc", "19456",
"testXML.xml", "766"};
File f = new File(getClass().getResource("tika-documents.rar").toURI());
try (Archive archive = new Archive(f)) {
assertThat(archive.isPasswordProtected()).isFalse();
assertThat(archive.isEncrypted()).isFalse();
for (int i = 0; i < expected.length; i += 2) {
FileHeader fileHeader = archive.nextFileHeader();
assertThat(fileHeader).isNotNull();
assertThat(fileHeader.getFileName()).contains(expected[i]);
assertThat(fileHeader.getUnpSize()).isEqualTo(Long.parseLong(expected[i + 1]));
assertThat(fileHeader.getFullUnpackSize()).isEqualTo(fileHeader.getUnpSize());
}
assertThat(archive.nextFileHeader()).isNull();
}
}
@ValueSource(strings = {
"audio/BoatModernEnglish-audio-text-unpack30.rar", // special audio/text compression enabled, RAR 2.9
"audio/BoatModernEnglish-audio-text-unpack20.rar", // special audio/text compression enabled, RAR 2.0
"audio/BoatModernEnglish-regular-unpack30.rar", // special audio/text compression disabled, RAR 2.9
"audio/BoatModernEnglish-regular-unpack20.rar", // special audio/text compression disabled, RAR 2.0
"audio/BoatModernEnglish-regular-unpack15-dos.rar", // special audio/text compression disabled, RAR 1.5 DOS
"audio/BoatModernEnglish-regular-unpack15-win.rar" // special audio/text compression disabled, RAR 1.5 Windows
})
@ParameterizedTest
public void testAudioDecompression(String fileName) throws Exception {
File f = new File(getClass().getResource(fileName).toURI());
try (Archive archive = new Archive(f)) {
assertThat(archive.isPasswordProtected()).isFalse();
assertThat(archive.isEncrypted()).isFalse();
FileHeader fileHeader = archive.nextFileHeader();
boolean isDos = fileName.endsWith("-dos.rar");
if (isDos) {
assertThat(fileHeader.getHostOS()).isEqualTo(HostSystem.msdos);
assertThat(fileHeader.getFileName()).isEqualTo("BOATMO~1.WAV");
} else {
assertThat(fileHeader.getHostOS()).isEqualTo(HostSystem.win32);
assertThat(fileHeader.getFileName()).isEqualTo("BoatModernEnglish.wav");
}
assertThat(fileHeader.getUnpSize()).isEqualTo(56464);
assertThat(fileHeader.getFullUnpackSize()).isEqualTo(fileHeader.getUnpSize());
byte[] audioData;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
archive.extractFile(fileHeader, baos);
audioData = baos.toByteArray();
}
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
try (InputStream is = getClass().getResource("audio/BoatModernEnglish.wav").openStream()) {
IOUtils.copy(is, baos);
}
byte[] expectedAudioData = baos.toByteArray();
assertThat(audioData).containsExactly(expectedAudioData);
}
fileHeader = archive.nextFileHeader();
if (isDos) {
assertThat(fileHeader.getHostOS()).isEqualTo(HostSystem.msdos);
assertThat(fileHeader.getFileName()).isEqualTo("LICENSE.TXT");
} else {
assertThat(fileHeader.getHostOS()).isEqualTo(HostSystem.win32);
assertThat(fileHeader.getFileName()).isEqualTo("LICENSE.txt");
}
assertThat(fileHeader.getUnpSize()).isEqualTo(107);
assertThat(fileHeader.getFullUnpackSize()).isEqualTo(fileHeader.getUnpSize());
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
archive.extractFile(fileHeader, baos);
assertThat(baos.toString()).isEqualTo("UofG Language Modules, CC BY-SA 4.0 "
+ "<https://creativecommons.org/licenses/by-sa/4.0>, via Wikimedia Commons");
}
assertThat(archive.nextFileHeader()).isNull();
}
}
/*
The file is shifted by 1-hour because it was created in Europe/Amsterdam.
Times in a RAR file are stored in the MS-DOS style, so the fields are always the same but the resulting timestamp is not.
Original timestamps:
MTime: 2022-02-23T09:24:19.191543300Z
CTime: 2022-02-23T09:34:59.759754700Z
ATime: 2022-03-02T17:45:18.694091100Z
Ensure the fields remain constant across timezones.
*/
@Nested
class ExtendedTimeTest {
@Test
@DefaultTimeZone("America/Los_Angeles")
public void testArchiveExtTimes_LosAngeles() throws Exception {
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("America/Los_Angeles"));
testArchiveExtTimes();
}
@Test
@DefaultTimeZone("America/Sao_Paulo")
public void testArchiveExtTimes_SaoPaulo() throws Exception {
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("America/Sao_Paulo"));
testArchiveExtTimes();
}
@Test
@DefaultTimeZone("Europe/Amsterdam")
public void testArchiveExtTimes_Amsterdam() throws Exception {
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("Europe/Amsterdam"));
testArchiveExtTimes();
}
@Test
@DefaultTimeZone("Asia/Kolkata")
public void testArchiveExtTimes_Kolkata() throws Exception {
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("Asia/Kolkata"));
testArchiveExtTimes();
}
private void testArchiveExtTimes() throws IOException, RarException {
try (InputStream is = getClass().getResourceAsStream("rar4-ext_time.rar")) {
try (Archive archive = new Archive(is)) {
assertThat(archive.getMainHeader().isSolid()).isFalse();
FileHeader fileHeader = archive.getFileHeaders().stream()
.filter(FileHeader::isFileHeader)
.findFirst()
.orElse(null);
assertThat(fileHeader).isNotNull();
assertThat(fileHeader.getFileName()).isEqualTo("files\\test\\short-text.txt");
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
archive.extractFile(fileHeader, baos);
assertThat(baos.toString()).isEqualTo("Short text for example");
}
assertThat(fileHeader.getMTime()).isEqualTo(toDate(2022, FEBRUARY, 23, 10, 24, 19, 191));
assertThat(fileHeader.getLastModifiedTime()).isEqualTo(toFileTime(fileHeader.getMTime(), 543300));
assertThat(fileHeader.getCTime()).isEqualTo(toDate(2022, FEBRUARY, 23, 10, 34, 59, 759));
assertThat(fileHeader.getCreationTime()).isEqualTo(toFileTime(fileHeader.getCTime(), 754700));
assertThat(fileHeader.getATime()).isEqualTo(toDate(2022, MARCH, 2, 18, 45, 18, 694));
assertThat(fileHeader.getLastAccessTime()).isEqualTo(toFileTime(fileHeader.getATime(), 91100));
}
}
}
private Date toDate(int year, int month, int day, int hour, int minute, int second, int millis) {
Calendar calendar = Calendar.getInstance();
calendar.set(year, month, day, hour, minute, second);
calendar.set(Calendar.MILLISECOND, millis);
return calendar.getTime();
}
private FileTime toFileTime(Date date, long nanos) {
return FileTime.from(Instant.ofEpochMilli(date.getTime()).plus(nanos, ChronoUnit.NANOS));
}
}
@Nested
class Solid {
@Test
public void givenSolidRar4File_whenExtractingInOrder_thenExtractionIsDone() throws Exception {
try (InputStream is = getClass().getResourceAsStream("solid/rar4-solid.rar")) {
try (Archive archive = new Archive(is)) {
assertThat(archive.getMainHeader().isSolid()).isTrue();
List<FileHeader> fileHeaders = archive.getFileHeaders();
assertThat(fileHeaders).hasSize(9);
for (int i = 0; i < fileHeaders.size(); i++) {
int index = i + 1;
FileHeader fileHeader = fileHeaders.get(i);
assertThat(fileHeader.getFileName()).isEqualTo("file" + index + ".txt");
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
archive.extractFile(fileHeaders.get(i), baos);
assertThat(baos.toString()).isEqualTo("file" + index + "\n");
}
}
}
}
}
@Test
public void givenSolidRar4File_whenExtractingInOrder_thenExtractionIsDone_withInputStream() throws Exception {
try (InputStream is = getClass().getResourceAsStream("solid/rar4-solid.rar")) {
try (Archive archive = new Archive(is)) {
assertThat(archive.getMainHeader().isSolid()).isTrue();
List<FileHeader> fileHeaders = archive.getFileHeaders();
assertThat(fileHeaders).hasSize(9);
for (int i = 0; i < fileHeaders.size(); i++) {
int index = i + 1;
FileHeader fileHeader = fileHeaders.get(i);
assertThat(fileHeader.getFileName()).isEqualTo("file" + index + ".txt");
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
try (InputStream fis = archive.getInputStream(fileHeaders.get(i))) {
IOUtils.copy(fis, baos);
}
assertThat(baos.toString()).isEqualTo("file" + index + "\n");
}
}
}
}
}
@Test
public void givenSolidRar4File_whenExtractingOutOfOrder_thenExceptionIsThrown() throws Exception {
try (InputStream is = getClass().getResourceAsStream("solid/rar4-solid.rar")) {
try (Archive archive = new Archive(is)) {
assertThat(archive.getMainHeader().isSolid()).isTrue();
List<FileHeader> fileHeaders = archive.getFileHeaders();
assertThat(fileHeaders).hasSize(9);
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
Throwable thrown = catchThrowable(() -> archive.extractFile(fileHeaders.get(4), baos));
assertThat(thrown).isExactlyInstanceOf(CrcErrorException.class);
}
}
}
}
@Test
public void givenSolidRar5File_whenCreatingArchive_thenUnsupportedRarV5ExceptionIsThrown() throws Exception {
try (InputStream is = getClass().getResourceAsStream("solid/rar5-solid.rar")) {
Throwable thrown = catchThrowable(() -> new Archive(is));
assertThat(thrown).isExactlyInstanceOf(UnsupportedRarV5Exception.class);
}
}
}
/**
* This class will test archives that are encrypted or password protected.
* <p>
* Encrypted archives are password protected, but also encrypt the list of files,
* so you need the password to list the content.
* <p>
* You can list the content of a password protected archive, but you cannot extract
* without the password.
*/
@Nested
class PasswordProtected {
@Test
public void givenEncryptedRar4File_whenCreatingArchiveWithPassword_thenItCanExtractContent() throws Exception {
try (InputStream is = getClass().getResourceAsStream("password/rar4-encrypted-junrar.rar")) {
try (Archive archive = new Archive(is, "junrar")) {
assertThat(archive.isEncrypted()).isTrue();
assertThat(archive.isPasswordProtected()).isTrue();
List<FileHeader> fileHeaders = archive.getFileHeaders();
assertThat(fileHeaders).hasSize(1);
FileHeader fileHeader = fileHeaders.get(0);
assertThat(fileHeader.isEncrypted()).isTrue();
assertThat(fileHeader.getFileName()).isEqualTo("file1.txt");
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
archive.extractFile(fileHeader, baos);
assertThat(baos.toString()).isEqualTo("file1\n");
}
}
}
}
@Test
public void givenPasswordProtectedRar4File_whenCreatingArchive_thenItCanListContent() throws Exception {
try (InputStream is = getClass().getResourceAsStream("password/rar4-password-junrar.rar")) {
try (Archive archive = new Archive(is)) {
assertThat(archive.isEncrypted()).isFalse();
assertThat(archive.isPasswordProtected()).isTrue();
List<FileHeader> fileHeaders = archive.getFileHeaders();
assertThat(fileHeaders).hasSize(1);
FileHeader fileHeader = fileHeaders.get(0);
assertThat(fileHeader.isEncrypted()).isTrue();
assertThat(fileHeader.getFileName()).isEqualTo("file1.txt");
}
}
}
@Test
public void givenPasswordProtectedRar4File_whenCreatingArchiveWithPassword_thenItCanExtractContent() throws Exception {
try (InputStream is = getClass().getResourceAsStream("password/rar4-password-junrar.rar")) {
try (Archive archive = new Archive(is, "junrar")) {
assertThat(archive.isEncrypted()).isFalse();
assertThat(archive.isPasswordProtected()).isTrue();
List<FileHeader> fileHeaders = archive.getFileHeaders();
assertThat(fileHeaders).hasSize(1);
FileHeader fileHeader = fileHeaders.get(0);
assertThat(fileHeader.isEncrypted()).isTrue();
assertThat(fileHeader.getFileName()).isEqualTo("file1.txt");
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
archive.extractFile(fileHeader, baos);
assertThat(baos.toString()).isEqualTo("file1\n");
}
}
}
}
@Test
public void givenEncryptedRar5File_whenCreatingArchive_thenUnsupportedRarV5ExceptionIsThrown() throws Exception {
try (InputStream is = getClass().getResourceAsStream("password/rar5-encrypted-junrar.rar")) {
Throwable thrown = catchThrowable(() -> new Archive(is));
assertThat(thrown).isExactlyInstanceOf(UnsupportedRarV5Exception.class);
}
}
@Test
public void givenPasswordProtectedRar5File_whenCreatingArchive_thenUnsupportedRarV5ExceptionIsThrown() throws Exception {
try (InputStream is = getClass().getResourceAsStream("password/rar5-password-junrar.rar")) {
Throwable thrown = catchThrowable(() -> new Archive(is));
assertThat(thrown).isExactlyInstanceOf(UnsupportedRarV5Exception.class);
}
}
}
@Nested
class Unicode {
@Test
public void unicodeFileNamesAreDecodedProperly() throws Exception {
File f = new File(getClass().getResource("unicode.rar").getPath());
try (Archive archive = new Archive(f)) {
List<String> names = archive.getFileHeaders().stream()
.map(FileHeader::getFileName)
.collect(Collectors.toList());
assertThat(names).containsExactlyInAnyOrder("������������������.txt", "������������.txt");
}
}
@Test
public void gh108_unicodeFileNamesAreDecodedProperly() throws Exception {
File f = new File(getClass().getResource("gh108.rar").getPath());
try (Archive archive = new Archive(f)) {
List<String> names = archive.getFileHeaders().stream()
.map(FileHeader::getFileName)
.collect(Collectors.toList());
assertThat(names).containsExactly("���.txt");
}
}
}
}