/*
* Copyright (C) 2012 - 2013 University of Dundee & Open Microscopy Environment.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package ome.services.blitz.test.utests;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Set;
import ome.services.blitz.repo.path.FilePathRestrictionInstance;
import ome.services.blitz.repo.path.FilePathRestrictions;
import ome.services.blitz.repo.path.MakePathComponentSafe;
import ome.services.blitz.util.CurrentPlatform;
import omero.util.TempFileManager;
import nl.javadude.assumeng.Assumption;
import nl.javadude.assumeng.AssumptionListener;
import org.testng.Assert;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
/**
* @author m.t.b.carroll@dundee.ac.uk
* @since 5.0
*/
@Test(groups = {"fs"})
@Listeners(AssumptionListener.class)
public class MakePathComponentSafeTest extends MakePathComponentSafe {
private static final Set<Integer> codePointsOfTypeControl;
private static final TempFileManager tempFileManager =
new TempFileManager("test-" + MakePathComponentSafeTest.class.getSimpleName());
static {
codePointsOfTypeControl = new HashSet<Integer>();
for (int codePoint = 0; codePoint < 0x100; codePoint++)
if (Character.getType(codePoint) == Character.CONTROL)
codePointsOfTypeControl.add(codePoint);
}
public MakePathComponentSafeTest() {
super(FilePathRestrictionInstance.getFilePathRestrictions(FilePathRestrictionInstance.values()));
}
/**
* Test that the transformation matrix does not correct characters to unsafe ones.
*/
@Test
public void testTransformationMatrixLegality() {
final Set<Integer> unsafeCodePoints = this.rules.transformationMatrix.keySet();
for (final Integer substitute : this.rules.transformationMatrix.values())
Assert.assertFalse(unsafeCodePoints.contains(substitute),
"character substitutions may not be to unsafe characters");
}
/**
* Test that the safe character is not included in any code points or strings deemed to be unsafe.
* (This is an unnecessarily strong criterion, but it is easily met.)
*/
@Test
public void testUnsafeCharacterAvoidance() {
final Set<Integer> unsafeCodePoints = this.rules.transformationMatrix.keySet();
Assert.assertFalse(unsafeCodePoints.contains(this.rules.safeCharacter),
"the safe character must not be transformed");
final Set<String> unsafeStrings = new HashSet<String>();
unsafeStrings.addAll(this.rules.unsafeNames);
unsafeStrings.addAll(this.rules.unsafePrefixes);
unsafeStrings.addAll(this.rules.unsafeSuffixes);
for (final String unsafeString : unsafeStrings)
Assert.assertEquals(unsafeString.indexOf(this.rules.safeCharacter), -1,
"the safe character may not appear in unsafe strings");
}
/**
* Test that the unsafe strings for matching are all in upper case,
* as the caller's string is upper-cased for matching in {@link MakePathComponentSafe#apply}.
*/
@Test
public void testUnsafeStringCase() {
final Set<String> unsafeStrings = new HashSet<String>();
unsafeStrings.addAll(this.rules.unsafeNames);
unsafeStrings.addAll(this.rules.unsafePrefixes);
unsafeStrings.addAll(this.rules.unsafeSuffixes);
for (final String unsafeString : unsafeStrings)
Assert.assertEquals(unsafeString, unsafeString.toUpperCase(),
"the unsafe strings should be upper-case");
}
/**
* Test that the parent directory of the given {@link File} contains a file of the given name.
* This is used to detect strange phenomena such as accidental reference to Windows NTFS file streams.
* @param file a file
* @param expectedName a filename
* @return if the file's parent directory contains the filename
*/
private boolean isFileNameReallyThere(File file, String expectedName) {
if (!file.isFile())
throw new IllegalArgumentException("must supply an argument that is actually a file");
for (final String fileInParent : file.getParentFile().list())
if (expectedName.equals(fileInParent))
return true;
return false;
}
/**
* Create the given file, check that data can be stored in it and retrieved from it,
* check that a file with the given name exists in the file's directory, then delete it.
* @param file a file
* @param name a filename
* @throws IOException if there was a problem in writing and reading the file
*/
private void testDataStorage(File file, String name) throws IOException {
final long testContentsOut = System.nanoTime();
final long testContentsIn;
final DataOutputStream out = new DataOutputStream(new FileOutputStream(file));
out.writeLong(testContentsOut);
out.close();
final boolean isFileCreated = isFileNameReallyThere(file, name);
final DataInputStream in = new DataInputStream(new FileInputStream(file));
testContentsIn = in.readLong();
in.close();
file.delete();
Assert.assertEquals(testContentsIn, testContentsOut,
"failed to be able to store data in file named " + name);
Assert.assertTrue(isFileCreated,
"ought to be able to create files named " + name);
}
/**
* Test that data cannot be stored in files named using unsafe characters.
* @param ruleId the rules for the platform to be tested
* @throws IOException unexpected
*/
private void testUnsafeCharacterUnsafety(FilePathRestrictionInstance ruleId) throws IOException {
final FilePathRestrictions rules = FilePathRestrictionInstance.getFilePathRestrictions(ruleId);
final File tempDir = tempFileManager.createPath("testUnsafeCharacterUnsafetyWindows", null, true);
for (final int unsafeCodePoint : rules.transformationMatrix.keySet()) {
if (codePointsOfTypeControl.contains(unsafeCodePoint))
/* no point testing, one wants to avoid control characters in filenames whatever the operating system permits */
continue;
final String unsafeString = new String(new int[] {unsafeCodePoint}, 0, 1);
/* don't use the unsafe code point as a prefix or suffix */
final String unsafeName = "unsafe" + this.rules.safeCharacter + unsafeString +
this.rules.safeCharacter + "unsafe";
final File unsafeFile = new File(tempDir, unsafeName);
try {
final OutputStream out = new FileOutputStream(unsafeFile);
out.close();
final boolean isFileCreated = isFileNameReallyThere(unsafeFile, unsafeName);
unsafeFile.delete();
Assert.assertFalse(isFileCreated,
"ought not to be able to create files whose name contains code point " + unsafeCodePoint);
} catch (FileNotFoundException e) {
// expected
}
}
tempFileManager.removePath(tempDir);
}
/**
* On Microsoft Windows, test that data cannot be stored in files named using unsafe characters.
* @throws IOException unexpected
*/
@Test
@Assumption(methods = {"isWindows"}, methodClass = CurrentPlatform.class)
public void testUnsafeCharacterUnsafetyWindows() throws IOException {
testUnsafeCharacterUnsafety(FilePathRestrictionInstance.WINDOWS_REQUIRED);
}
/**
* On Linux, test that data cannot be stored in files named using unsafe characters.
* @throws IOException unexpected
*/
@Test
@Assumption(methods = {"isLinux"}, methodClass = CurrentPlatform.class)
public void testUnsafeCharacterUnsafetyLinux() throws IOException {
testUnsafeCharacterUnsafety(FilePathRestrictionInstance.UNIX_REQUIRED);
}
/**
* On Apple Mac OS X, test that data cannot be stored in files named using unsafe characters.
* @throws IOException unexpected
*/
@Test
@Assumption(methods = {"isMacOSX"}, methodClass = CurrentPlatform.class)
public void testUnsafeCharacterUnsafetyMacOSX() throws IOException {
testUnsafeCharacterUnsafety(FilePathRestrictionInstance.UNIX_REQUIRED);
}
/**
* Test that one of the operating-system-specific tests for the unsafety of unsafe characters
* did actually execute because the current operating system was actually recognized.
*/
@Test
@Assumption(methods = {"isUnknown"}, methodClass = CurrentPlatform.class)
public void testPlatformTestExecuted() {
Assert.fail("one of the operating-system-specific tests should have executed");
}
/**
* Test that data can be stored in files named using sanitized unsafe characters.
* @throws IOException unexpected
*/
@Test
public void testSanitizedUnsafeCharacterSafety() throws IOException {
final File tempDir = tempFileManager.createPath("testSanitizedUnsafeCharacterSafety", null, true);
for (final int safeCodePoint : this.rules.transformationMatrix.keySet()) {
final String unsafeString = new String(new int[] {safeCodePoint}, 0, 1);
final String unsafeName = "safe" + this.rules.safeCharacter + unsafeString +
this.rules.safeCharacter + "safe";
final String safeName = apply(unsafeName);
Assert.assertEquals(apply(safeName), safeName,
"sanitization should not change already-sanitized names");
final File safeFile = new File(tempDir, safeName);
testDataStorage(safeFile, safeName);
}
tempFileManager.removePath(tempDir);
}
/**
* Test that data can be stored in files named using unsanitized safe characters.
* @throws IOException unexpected
*/
@Test
public void testSafeCharacterSafety() throws IOException {
final File tempDir = tempFileManager.createPath("testSafeCharacterSafety", null, true);
final Set<Integer> safeCodePoints = new HashSet<Integer>();
safeCodePoints.add(Character.codePointAt(new char[] {this.rules.safeCharacter}, 0));
safeCodePoints.addAll(this.rules.transformationMatrix.values());
for (final int safeCodePoint : safeCodePoints) {
final String safeString = new String(new int[] {safeCodePoint}, 0, 1);
final String safeName = "safe" + this.rules.safeCharacter + safeString +
this.rules.safeCharacter + "safe";
final File safeFile = new File(tempDir, safeName);
testDataStorage(safeFile, safeName);
}
tempFileManager.removePath(tempDir);
}
/**
* Test that data can be stored in files named using sanitized unsafe names.
* @throws IOException unexpected
*/
@Test
public void testSanitizedUnsafeNameSafety() throws IOException {
final File tempDir = tempFileManager.createPath("testSanitizedUnsafeNameSafety", null, true);
for (final String unsafeName : this.rules.unsafeNames) {
final String safeName = apply(unsafeName);
final File safeFile = new File(tempDir, safeName);
testDataStorage(safeFile, safeName);
}
tempFileManager.removePath(tempDir);
}
/**
* Test that data can be stored in files named using sanitized unsafe prefixes.
* @throws IOException unexpected
*/
@Test
public void testSanitizedUnsafePrefixSafety() throws IOException {
final File tempDir = tempFileManager.createPath("testSanitizedUnsafePrefixSafety", null, true);
for (final String unsafePrefix : this.rules.unsafePrefixes) {
final String unsafeName = unsafePrefix + this.rules.safeCharacter + "unsafe";
final String safeName = apply(unsafeName);
Assert.assertEquals(apply(safeName), safeName,
"sanitization should not change already-sanitized names");
Assert.assertTrue(safeName.endsWith("unsafe"),
"file path sanitization should preserve safe suffixes");
final File safeFile = new File(tempDir, safeName);
testDataStorage(safeFile, safeName);
}
tempFileManager.removePath(tempDir);
}
/**
* Test that data can be stored in files named using sanitized unsafe suffixes.
* @throws IOException unexpected
*/
@Test
public void testSanitizedUnsafeSuffixSafety() throws IOException {
final File tempDir = tempFileManager.createPath("testSanitizedUnsafeSuffixSafety", null, true);
for (final String unsafeSuffix : this.rules.unsafeSuffixes) {
final String unsafeName = "unsafe" + this.rules.safeCharacter + unsafeSuffix;
final String safeName = apply(unsafeName);
Assert.assertEquals(apply(safeName), safeName,
"sanitization should not change already-sanitized names");
Assert.assertTrue(safeName.startsWith("unsafe"),
"file path sanitization should preserve safe prefixes");
final File safeFile = new File(tempDir, safeName);
testDataStorage(safeFile, safeName);
}
tempFileManager.removePath(tempDir);
}
/**
* Test that safely named files are not renamed in name sanitization.
* Checks that those names are preserved upon which BioFormats depends.
* @throws IOException unexpected
*/
@Test
public void testSensitiveNameSafety() {
// Leica OME
final String sensitiveName = "{Group}GroupData.xml";
Assert.assertEquals(apply(sensitiveName), sensitiveName,
"sensitive names should not be changed by sanitization");
}
}