Skip to content

Commit 4d01f36

Browse files
authored
Merge pull request #76 from cxcorp/zipper-path-separators
Improve StudentFileAwareZipper
2 parents fce2329 + 7998eb0 commit 4d01f36

File tree

8 files changed

+145
-10
lines changed

8 files changed

+145
-10
lines changed

tmc-langs-framework/src/main/java/fi/helsinki/cs/tmc/langs/io/zip/StudentFileAwareZipper.java

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,93 @@
1616
import java.nio.file.Files;
1717
import java.nio.file.Path;
1818

19+
/**
20+
* This {@link Zipper} implementation recursively zips the files of a directory.
21+
* Only files considered to be student files by the specified {@link StudentFilePolicy}
22+
* are included in the archive.
23+
*
24+
* <p>The {@link StudentFilePolicy} provided either via the
25+
* {@link #StudentFileAwareZipper(StudentFilePolicy) constructor} or a
26+
* {@link #setStudentFilePolicy(StudentFilePolicy) setter method} is used to determine whether
27+
* a file or directory should be included in the archive.</p>
28+
*
29+
* <p>Individual directories can be excluded from archival by adding a file in the directory
30+
* root with the name of {@code .tmcnosubmit}. For example, if the folder {@code sensitive/}
31+
* should be excluded, the file {@code sensitive/.tmcnosubmit} should be created. The contents
32+
* of the file are ignored.</p>
33+
*
34+
* <p>File system roots (e.g. {@code /} on *nix and {@code C:\} on Windows platforms) cannot
35+
* be zipped, as this {@link Zipper} includes the specified {@code rootDirectory}
36+
* in the zip file as a parent directory.</p>
37+
*/
1938
public final class StudentFileAwareZipper implements Zipper {
2039

2140
private static final Logger log = LoggerFactory.getLogger(StudentFileAwareZipper.class);
41+
// The zip standard mandates the forward slash "/" to be used as path separator
42+
private static final char ZIP_SEPARATOR = '/';
43+
2244
private StudentFilePolicy filePolicy;
2345

46+
/**
47+
* Instantiates a new {@link StudentFileAwareZipper} without a {@link StudentFilePolicy}.
48+
* The {@link #setStudentFilePolicy(StudentFilePolicy)} method can be used to set the policy.
49+
*/
2450
public StudentFileAwareZipper() {}
2551

52+
/**
53+
* Instantiates a new {@link StudentFileAwareZipper} with the
54+
* specified {@link StudentFilePolicy}.
55+
*
56+
* @param filePolicy Determines which files and directories are included in the archive.
57+
* @see #setStudentFilePolicy(StudentFilePolicy)
58+
*/
2659
public StudentFileAwareZipper(StudentFilePolicy filePolicy) {
2760
this.filePolicy = filePolicy;
2861
}
2962

63+
/**
64+
* Sets the {@link StudentFilePolicy} which determines which files and directories
65+
* are included in the archive.
66+
*
67+
* @param studentFilePolicy Determines which files and directories are included in the archive.
68+
* @see #StudentFileAwareZipper(StudentFilePolicy)
69+
*/
3070
@Override
3171
public void setStudentFilePolicy(StudentFilePolicy studentFilePolicy) {
3272
this.filePolicy = studentFilePolicy;
3373
}
3474

75+
/**
76+
* Recursively zips all files and directories which are considered to be student files.
77+
*
78+
* @param rootDirectory The root directory of the files and directories to zip.
79+
* Included in the archive. Cannot be a file system root.
80+
* @return Byte array containing the bytes of the {@link ZipArchiveOutputStream}.
81+
* @throws IOException if reading a file or directory fails.
82+
* @throws IllegalArgumentException if attempting to zip a file system root.
83+
* @see #setStudentFilePolicy(StudentFilePolicy)
84+
*/
3585
@Override
3686
public byte[] zip(Path rootDirectory) throws IOException {
3787
log.debug("Starting to zip {}", rootDirectory);
3888

3989
if (!Files.exists(rootDirectory)) {
40-
log.error("Attempted to zip nonexistent directory {}", rootDirectory);
90+
log.error("Attempted to zip nonexistent directory \"{}\"", rootDirectory);
4191
throw new FileNotFoundException("Attempted to zip nonexistent directory");
4292
}
4393

94+
if (rootDirectory.toAbsolutePath().getNameCount() == 0) {
95+
// getNameCount returns 0 if the path only represents a root component
96+
log.error("Attempted to zip a root \"{}\"", rootDirectory);
97+
throw new IllegalArgumentException("Filesystem root zipping is not supported");
98+
}
99+
100+
if (filePolicy == null) {
101+
log.error("Attepted to zip before setting the filePolicy");
102+
throw new IllegalStateException(
103+
"The student file policy must be set before zipping files");
104+
}
105+
44106
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
45107
try (ZipArchiveOutputStream zipStream = new ZipArchiveOutputStream(buffer)) {
46108
zipRecursively(rootDirectory, zipStream, rootDirectory);
@@ -109,13 +171,8 @@ private void writeToZip(Path currentPath, ZipArchiveOutputStream zipStream, Path
109171

110172
log.trace("Writing {} to zip", currentPath);
111173

112-
String name = projectPath.getParent().relativize(currentPath).toString();
113-
114-
if (Files.isDirectory(currentPath)) {
115-
log.trace("{} is a directory", currentPath);
116-
// Must be "/", can not be replaces with File.separator
117-
name += "/";
118-
}
174+
Path relativePath = projectPath.getParent().relativize(currentPath);
175+
String name = relativePathToZipCompliantName(relativePath, Files.isDirectory(currentPath));
119176

120177
ZipArchiveEntry entry = new ZipArchiveEntry(name);
121178
zipStream.putArchiveEntry(entry);
@@ -129,4 +186,25 @@ private void writeToZip(Path currentPath, ZipArchiveOutputStream zipStream, Path
129186
log.trace("Closing entry");
130187
zipStream.closeArchiveEntry();
131188
}
189+
190+
private static String relativePathToZipCompliantName(Path path, boolean isDirectory) {
191+
log.trace("Generating zip-compliant filename from Path \"{}\", isDirectory: {}",
192+
path, isDirectory);
193+
194+
StringBuilder sb = new StringBuilder();
195+
for (Path part : path) {
196+
sb.append(part);
197+
sb.append(ZIP_SEPARATOR);
198+
}
199+
200+
if (!isDirectory) {
201+
// ZipArchiveEntry assumes the entry represents a directory if and only
202+
// if the name ends with a forward slash "/". Remove the trailing slash
203+
// because this wasn't a directory.
204+
log.trace("Path wasn't a directory, removing trailing slash");
205+
sb.deleteCharAt(sb.length() - 1);
206+
}
207+
208+
return sb.toString();
209+
}
132210
}

tmc-langs-framework/src/test/java/fi/helsinki/cs/tmc/langs/io/zip/StudentFileAwareZipperTest.java

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static org.junit.Assert.fail;
66

77
import fi.helsinki.cs.tmc.langs.io.EverythingIsStudentFileStudentFilePolicy;
8+
import fi.helsinki.cs.tmc.langs.io.StudentFilePolicy;
89
import fi.helsinki.cs.tmc.langs.utils.TestUtils;
910

1011
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
@@ -20,6 +21,7 @@
2021
import java.nio.file.FileVisitor;
2122
import java.nio.file.Files;
2223
import java.nio.file.Path;
24+
import java.nio.file.Paths;
2325
import java.nio.file.attribute.BasicFileAttributes;
2426
import java.util.Enumeration;
2527
import java.util.HashMap;
@@ -77,10 +79,23 @@ public FileVisitResult postVisitDirectory(Path path, IOException ex)
7779
}
7880

7981
@Test(expected = FileNotFoundException.class)
80-
public void zipperThrowsExceptionWhenUnzippingNonExistentFile() throws IOException {
82+
public void zipperThrowsExceptionWhenZippingNonExistentFile() throws IOException {
8183
zipper.zip(TEST_ASSETS_DIR.resolve("noSuchDir"));
8284
}
8385

86+
@Test(expected = IllegalArgumentException.class)
87+
public void zipperThrowsExceptionWhenZippingRoot() throws IOException {
88+
// platform-specific root
89+
zipper.zip(Paths.get("/").toAbsolutePath());
90+
}
91+
92+
@Test(expected = IllegalStateException.class)
93+
public void zipperThrowsExceptionWhenZippingWithoutSettingPolicy() throws IOException {
94+
Path existingPath = TestUtils.getPath(StudentFileAwareUnzipperTest.class,
95+
"tmcnosubmit_test_case");
96+
new StudentFileAwareZipper().zip(existingPath);
97+
}
98+
8499
@Test
85100
public void zipperCorrectlyZipsSingleFile() throws IOException {
86101

@@ -100,7 +115,7 @@ public void zipperCorrectlyZipsSingleFile() throws IOException {
100115
@Test
101116
public void zipperCorrectlyZipsFolderWithFilesAndSubFolders() throws IOException {
102117
// Create empty dir that is not in git
103-
Path emptyDir = (TEST_DIR.resolve("dir"));
118+
Path emptyDir = TEST_DIR.resolve("dir");
104119
if (Files.notExists(emptyDir)) {
105120
Files.createDirectory(emptyDir);
106121
}
@@ -137,7 +152,43 @@ public void zipperDetectectsAndObeysTmcnosubmitFiles() throws IOException {
137152
expected.close();
138153
actual.close();
139154
Files.deleteIfExists(compressed);
155+
}
156+
157+
@Test
158+
public void zipperFollowsStudentPolicy() throws IOException {
159+
Path uncompressed = TestUtils.getPath(StudentFileAwareUnzipperTest.class,
160+
"zip_studentpolicy_test_case");
161+
162+
// Policy: zip every directory and file whose name starts with "include"
163+
zipper.setStudentFilePolicy(new StudentFilePolicy() {
164+
@Override
165+
public boolean isStudentFile(Path path, Path projectRootPath) {
166+
if (path.equals(projectRootPath)) {
167+
return true;
168+
}
169+
return path.getFileName().toString().startsWith("include");
170+
}
171+
172+
@Override
173+
public boolean mayDelete(Path file, Path projectRoot) {
174+
return true;
175+
}
176+
});
177+
178+
byte[] zip = zipper.zip(uncompressed);
179+
Path compressed = Files.createTempFile("testZip", ".zip");
180+
Files.write(compressed, zip);
181+
182+
Path referenceZip = TEST_ASSETS_DIR.resolve("zip_studentpolicy_test_case.zip");
140183

184+
ZipFile expected = new ZipFile(referenceZip.toFile());
185+
ZipFile actual = new ZipFile(compressed.toFile());
186+
187+
assertZipsEqualDecompressed(expected, actual);
188+
189+
expected.close();
190+
actual.close();
191+
Files.deleteIfExists(compressed);
141192
}
142193

143194
private void assertZipsEqualDecompressed(ZipFile expected, ZipFile actual)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
liirum laarum
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec venenatis ac ante id egestas. Praesent at enim id lorem egestas iaculis eu pharetra dolor. Nullam ullamcorper laoreet massa, sit amet sodales ex pharetra vitae. Aliquam id vehicula nunc, bibendum mattis orci. Nunc fringilla sem enim, a suscipit justo convallis id. Etiam maximus urna lorem, ac maximus nulla sagittis nec. Curabitur nec mauris finibus, fringilla mauris eu, elementum neque. Sed et sem tellus. Suspendisse eget ipsum vel odio mattis venenatis. Morbi egestas elementum felis eu aliquet.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Suspendisse tincidunt vehicula sem. Cras sit amet enim nec arcu euismod tincidunt nec sed eros. Curabitur tempor enim sit amet accumsan dictum. Fusce tempus lorem eget odio imperdiet fringilla. Proin lobortis consequat finibus. Praesent in nunc metus. Fusce hendrerit lacus id orci vulputate, vitae consequat velit tempor. Sed iaculis ac eros id sollicitudin. Proin vitae feugiat justo. Nunc ac diam aliquet, lacinia nibh eget, commodo nibh. Mauris ipsum leo, finibus non risus non, egestas semper nulla. Sed sit amet maximus tortor, a rhoncus neque. Morbi eu venenatis eros. Etiam imperdiet viverra vestibulum. Mauris tristique rhoncus ante et porta. Etiam consequat a orci eu scelerisque.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
In iaculis quam eu accumsan condimentum. Proin molestie lectus magna, ac rutrum ex elementum eget. Duis tincidunt ante ex. Suspendisse aliquam venenatis aliquam. Nullam viverra consequat tincidunt. Praesent ut odio ac arcu consequat hendrerit. Proin rutrum sapien convallis accumsan pharetra.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Lorem;Ipsum
2+
Yes;No

0 commit comments

Comments
 (0)