blob: f4c943271e294fb2b4d2b77c51497c25055a0a81 [file] [log] [blame]
// Copyright 2014 Google Inc. All Rights Reserved.
//
// Licensed 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.google.enterprise.adaptor.fs;
import static com.google.enterprise.adaptor.fs.AclView.user;
import static com.google.enterprise.adaptor.fs.AclView.group;
import static com.google.enterprise.adaptor.fs.AclView.GenericPermission.*;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
import static java.nio.file.attribute.AclEntryFlag.*;
import static java.nio.file.attribute.AclEntryPermission.*;
import static java.nio.file.attribute.AclEntryType.*;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.google.common.io.CharStreams;
import com.google.enterprise.adaptor.Acl;
import com.google.enterprise.adaptor.DocIdPusher;
import com.google.enterprise.adaptor.fs.WinApi.Netapi32Ex;
import org.junit.*;
import org.junit.rules.ExpectedException;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.WString;
import com.sun.jna.platform.win32.LMErr;
import com.sun.jna.platform.win32.Win32Exception;
import com.sun.jna.platform.win32.WinError;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.platform.win32.WinDef.DWORD;
import com.sun.jna.platform.win32.WinDef.ULONG;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.AclFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.Collections;
import java.util.Set;
/** Tests for {@link WindowsFileDelegate} */
public class WindowsFileDelegateTest extends TestWindowsAclViews {
@BeforeClass
public static void checkIfRunningOnWindows() {
TestHelper.assumeOsIsWindows();
}
private FileDelegate delegate = new WindowsFileDelegate();
private AccumulatingAsyncDocIdPusher pusher =
new AccumulatingAsyncDocIdPusher();
private Path tempRoot;
@Rule
public ExpectedException thrown = ExpectedException.none();
@Before
public void setUp() throws Exception {
tempRoot = getTempRoot();
}
@After
public void tearDown() {
delegate.destroy();
}
@Test
public void testGetDfsShareAclView() throws Exception {
// The *_OBJECT_ACE_TYPEs will get filtered out by newAclEntry().
AclFileAttributeView expectedAcl = new AclView(
group("AccessAllowedAce").type(ALLOW).perms(GENERIC_READ)
.flags(FILE_INHERIT, DIRECTORY_INHERIT),
user("AccessDeniedAce").type(DENY).perms(GENERIC_READ)
.flags(FILE_INHERIT, DIRECTORY_INHERIT));
AclFileAttributeView aclView = getDfsShareAclView(
new AceBuilder()
.setSid(AccountSid.group("AccessAllowedAce", null))
.setType(WinNT.ACCESS_ALLOWED_ACE_TYPE)
.setPerms(WinNT.GENERIC_READ)
.setFlags(WinNT.OBJECT_INHERIT_ACE, WinNT.CONTAINER_INHERIT_ACE)
.build(),
new AceBuilder()
.setSid(AccountSid.user("AccessAllowedObjectAce", null))
.setType(WinNT.ACCESS_ALLOWED_OBJECT_ACE_TYPE)
.setPerms(WinNT.GENERIC_ALL)
.setFlags(WinNT.OBJECT_INHERIT_ACE)
.build(),
new AceBuilder()
.setSid(AccountSid.user("AccessDeniedAce", null))
.setType(WinNT.ACCESS_DENIED_ACE_TYPE)
.setPerms(WinNT.GENERIC_READ)
.setFlags(WinNT.OBJECT_INHERIT_ACE, WinNT.CONTAINER_INHERIT_ACE)
.build(),
new AceBuilder()
.setSid(AccountSid.group("AccessDeniedObjectAce", null))
.setType(WinNT.ACCESS_DENIED_OBJECT_ACE_TYPE)
.setPerms(WinNT.GENERIC_ALL)
.setFlags(WinNT.OBJECT_INHERIT_ACE)
.build());
assertNotNull(aclView);
assertEquals(expectedAcl.getAcl(), aclView.getAcl());
}
@Test
public void testGetDfsShareAclViewUnsupportedAceType() throws Exception {
thrown.expect(IllegalArgumentException.class);
AclFileAttributeView aclView = getDfsShareAclView(
new AceBuilder()
.setSid(AccountSid.group("SystemAuditAce", null))
.setType(WinNT.SYSTEM_AUDIT_ACE_TYPE)
.setPerms(WinNT.GENERIC_ALL)
.setFlags(WinNT.OBJECT_INHERIT_ACE, WinNT.CONTAINER_INHERIT_ACE)
.build());
}
private static AclFileAttributeView getDfsShareAclView(
WinNT.ACCESS_ACEStructure... aces) throws Exception {
byte[] dacl = buildDaclMemory(aces);
final Memory memory = new Memory(dacl.length);
memory.write(0, dacl, 0, dacl.length);
Netapi32Ex netapi = new UnsupportedNetapi32() {
@Override
public int NetDfsGetSecurity(String dfsPath, int securityInfo,
PointerByReference bufptr, IntByReference bufsz) {
bufptr.setValue(memory);
bufsz.setValue((int)(memory.size()));
return WinError.ERROR_SUCCESS;
}
@Override
public int NetApiBufferFree(Pointer buf) {
return WinError.ERROR_SUCCESS;
}
};
WindowsAclFileAttributeViews wafav =
new TestAclFileAttributeViews(null, null, null, netapi, null);
WindowsFileDelegate delegate = new WindowsFileDelegate(null, netapi, wafav);
return delegate.getDfsShareAclView(Paths.get("\\\\host\\share"));
}
@Test
public void testGetDfsShareAclViewError() throws Exception {
Netapi32Ex netapi = new UnsupportedNetapi32() {
@Override
public int NetDfsGetSecurity(String dfsPath, int securityInfo,
PointerByReference bufptr, IntByReference bufsz) {
return WinError.ERROR_ACCESS_DENIED;
}
@Override
public int NetApiBufferFree(Pointer buf) {
return WinError.ERROR_SUCCESS;
}
};
WindowsAclFileAttributeViews wafav =
new TestAclFileAttributeViews(null, null, null, netapi, null);
WindowsFileDelegate delegate = new WindowsFileDelegate(null, netapi, wafav);
thrown.expect(Win32Exception.class);
delegate.getDfsShareAclView(Paths.get("\\\\host\\share"));
}
@Test
public void testGetDfsUncActiveStorageUncError() throws Exception {
Netapi32Ex netapi = new UnsupportedNetapi32() {
@Override
public int NetDfsGetInfo(String dfsPath, String server, String share,
int level, PointerByReference bufptr) {
return WinError.ERROR_ACCESS_DENIED;
}
};
assertNull(getDfsUncActiveStorageUnc(netapi));
}
@Test
public void testGetDfsUncActiveStorageUncNoStorage() throws Exception {
final Netapi32Ex.DFS_INFO_3 info = newDfsInfo3();
assertEquals(0, info.NumberOfStorages.intValue());
thrown.expect(IOException.class);
getDfsUncActiveStorageUnc(info);
}
@Test
public void testGetDfsUncActiveStorageUncSingleActiveStorage()
throws Exception {
final Netapi32Ex.DFS_INFO_3 info = newDfsInfo3(
new Storage(Netapi32Ex.DFS_STORAGE_STATE_ONLINE, "server", "share"));
assertEquals(1, info.NumberOfStorages.intValue());
assertEquals(Paths.get("\\\\server\\share"),
getDfsUncActiveStorageUnc(info));
}
@Test
public void testGetDfsUncActiveStorageUncNoActiveStorage() throws Exception {
final Netapi32Ex.DFS_INFO_3 info = newDfsInfo3(
new Storage(0, "server", "share"));
assertEquals(1, info.NumberOfStorages.intValue());
thrown.expect(IOException.class);
getDfsUncActiveStorageUnc(info);
}
@Test
public void testGetDfsUncActiveStorageUncSomeActiveStorage()
throws Exception {
final Netapi32Ex.DFS_INFO_3 info = newDfsInfo3(
new Storage(0, "inactive", "inactive"),
new Storage(Netapi32Ex.DFS_STORAGE_STATE_ONLINE, "server", "share"),
new Storage(Netapi32Ex.DFS_STORAGE_STATE_ONLINE, "active", "active"));
assertEquals(3, info.NumberOfStorages.intValue());
assertEquals(Paths.get("\\\\server\\share"),
getDfsUncActiveStorageUnc(info));
}
private static Path getDfsUncActiveStorageUnc(
final Netapi32Ex.DFS_INFO_3 info) throws Exception {
Netapi32Ex netapi = new UnsupportedNetapi32() {
@Override
public int NetDfsGetInfo(String dfsPath, String server, String share,
int level, PointerByReference bufptr) {
bufptr.setValue(info.getPointer());
return LMErr.NERR_Success;
}
@Override
public int NetApiBufferFree(Pointer buf) {
return WinError.ERROR_SUCCESS;
}
};
return getDfsUncActiveStorageUnc(netapi);
}
private static Path getDfsUncActiveStorageUnc(Netapi32Ex netapi)
throws Exception {
WindowsFileDelegate delegate = new WindowsFileDelegate(null, netapi, null);
Path dfsPath = Paths.get("\\\\host\\share");
return delegate.getDfsUncActiveStorageUnc(dfsPath);
}
@Test
public void testNewDocIdLocalFiles() throws Exception {
Path dir = newTempDir("testDir");
Path file = newTempFile(dir, "test");
String id = delegate.newDocId(tempRoot).getUniqueId();
assertTrue(id.startsWith(tempRoot.toString().replace('\\', '/')));
assertTrue(id.endsWith("/"));
id = delegate.newDocId(dir).getUniqueId();
assertTrue(id.startsWith(tempRoot.toString().replace('\\', '/')));
assertTrue(id.startsWith(dir.toString().replace('\\', '/')));
assertTrue(id.endsWith("/"));
id = delegate.newDocId(file).getUniqueId();
assertTrue(id.startsWith(tempRoot.toString().replace('\\', '/')));
assertTrue(id.startsWith(dir.toString().replace('\\', '/')));
assertTrue(id.equals(file.toString().replace('\\', '/')));
assertFalse(id.endsWith("/"));
}
@Test
public void testNewDocIdVirtualUncPaths() throws Exception {
assertEquals("\\\\host/share",
delegate.newDocId(Paths.get("\\\\host\\share")).getUniqueId());
assertEquals("\\\\host/share",
delegate.newDocId(Paths.get("\\\\host\\share\\")).getUniqueId());
assertEquals("\\\\host/share/foo/bar",
delegate.newDocId(Paths.get("\\\\host\\share\\foo\\bar"))
.getUniqueId());
}
@Test
public void testNewDocIdLocalUncPaths() throws Exception {
String uncTempRoot = getTempRootAsUncPath();
assumeNotNull(uncTempRoot);
Path tempRoot = Paths.get(uncTempRoot);
String expectedTempRootId = "\\\\" + uncTempRoot.substring(2)
.replace('\\', '/') + "/";
assertEquals(expectedTempRootId,
delegate.newDocId(tempRoot).getUniqueId());
newTempDir("testDir");
assertEquals(expectedTempRootId + "testDir/",
delegate.newDocId(tempRoot.resolve("testDir")).getUniqueId());
newTempFile("test");
assertEquals(expectedTempRootId + "test",
delegate.newDocId(tempRoot.resolve("test")).getUniqueId());
}
private String getTempRootAsUncPath() throws IOException {
String tempPath = temp.getRoot().getCanonicalPath();
if (tempPath.length() > 2 && tempPath.charAt(1) == ':') {
String uncPath = "\\\\localhost\\" + tempPath.substring(0, 1) + "$"
+ tempPath.substring(2);
try {
// Now verify we have access to the local administrative share.
if (new File(uncPath).list() != null) {
return uncPath;
}
} catch (SecurityException e) {
// Cannot access local administrative share.
}
}
return null;
}
@Test
public void testStartMonitorBadPath() throws Exception {
Path file = newTempFile("test.txt");
thrown.expect(IOException.class);
delegate.startMonitorPath(file, pusher);
}
@Test
public void testStartStopMonitor() throws Exception {
delegate.startMonitorPath(tempRoot, pusher);
delegate.stopMonitorPath();
}
@Test
public void testMonitorAddFile() throws Exception {
// These shouldn't show up as new or modified.
newTempDir("existingDir");
newTempFile("existingFile");
delegate.startMonitorPath(tempRoot, pusher);
Path file = newTempFile("test.txt");
// Adding a file shows up as a change to its parent.
checkForChanges(Collections.singleton(newRecord(tempRoot)));
}
@Test
public void testMonitorDeleteFile() throws Exception {
Path file = newTempFile("test.txt");
delegate.startMonitorPath(tempRoot, pusher);
Files.delete(file);
// Deleting a file shows up as a change to itself and its parent.
checkForChanges(Sets.newHashSet(newRecord(tempRoot), newRecord(file)));
}
@Test
public void testMonitorRenameFile() throws Exception {
Path file = newTempFile("test.txt");
Path newFile = file.resolveSibling("newName.txt");
delegate.startMonitorPath(tempRoot, pusher);
Files.move(file, newFile, StandardCopyOption.ATOMIC_MOVE);
// Renaming a file shows up as a change to its old name, its new name,
// and its parent.
checkForChanges(Sets.newHashSet(newRecord(tempRoot), newRecord(file),
newRecord(newFile)));
}
@Test
public void testMonitorMoveAccrossDirs() throws Exception {
Path dir1 = newTempDir("dir1");
Path dir2 = newTempDir("dir2");
Path file1 = newTempFile(dir1, "test.txt");
Path file2 = dir2.resolve(file1.getFileName());
delegate.startMonitorPath(tempRoot, pusher);
Files.move(file1, file2);
// Moving a file shows up as a change to its old name, its new name,
// its old parent, and its new parent.
checkForChanges(Sets.newHashSet(newRecord(file1), newRecord(file2),
newRecord(dir1), newRecord(dir2)));
}
@Test
public void testMonitorModifyFile() throws Exception {
Path file = newTempFile("test.txt");
delegate.startMonitorPath(tempRoot, pusher);
Files.write(file, "Hello World".getBytes("UTF-8"));
// Modifying a file shows up as a change to that file.
checkForChanges(Collections.singleton(newRecord(file)));
}
@Test
public void testMonitorModifyFileAttributes() throws Exception {
Path file = newTempFile("test.txt");
FileTime lastModified = Files.getLastModifiedTime(file);
delegate.startMonitorPath(tempRoot, pusher);
Files.setLastModifiedTime(file,
FileTime.fromMillis(lastModified.toMillis() + 10000L));
// Modifying a file shows up as a change to that file.
checkForChanges(Collections.singleton(newRecord(file)));
}
@Test
public void testMonitorRenameDir() throws Exception {
Path dir = newTempDir("dir1");
Path newDir = dir.resolveSibling("newName.dir");
delegate.startMonitorPath(tempRoot, pusher);
Files.move(dir, newDir, StandardCopyOption.ATOMIC_MOVE);
// Renaming a directory shows up as a change to its old name, its new name,
// and its parent.
checkForChanges(Sets.newHashSet(newRecord(tempRoot), newRecord(dir)));
}
@Test
public void testMonitorMoveDir() throws Exception {
Path dir1 = newTempDir("dir1");
Path dir2 = newTempDir("dir2");
Path dir1dir2 = dir1.resolve(dir2.getFileName());
delegate.startMonitorPath(tempRoot, pusher);
Files.move(dir2, dir1dir2);
// Moving a file shows up as a change to its old name, its new name,
// its old parent, and its new parent.
checkForChanges(Sets.newHashSet(newRecord(tempRoot), newRecord(dir1),
newRecord(dir2)));
}
@Test
public void testMonitorChangesInSubDirs() throws Exception {
Path dir = newTempDir("testDir");
Path file = newTempFile(dir, "test.txt");
delegate.startMonitorPath(tempRoot, pusher);
Files.write(file, "Hello World".getBytes("UTF-8"));
// Modifying a file shows up as a change to that file.
checkForChanges(Sets.newHashSet(newRecord(file), newRecord(dir)));
}
private void checkForChanges(Set<DocIdPusher.Record> expected)
throws Exception {
// Collect up the changes.
Set<DocIdPusher.Record> changes = Sets.newHashSet();
final long maxLatencyMillis = 10000;
long latencyMillis = maxLatencyMillis;
long batchLatencyMillis = 500;
boolean inFollowup = false;
while (latencyMillis > 0) {
Thread.sleep(batchLatencyMillis);
latencyMillis -= batchLatencyMillis;
changes.addAll(pusher.getRecords());
pusher.reset();
if (changes.size() == expected.size()) {
// If the changes size is equal to the expected size then
// keep listening for changes for the same period of time
// that it took to get the current notifications to see if
// we find any additional changes.
if (!inFollowup) {
latencyMillis = maxLatencyMillis - latencyMillis;
inFollowup = true;
}
}
if (changes.size() > expected.size()) {
// We've found more changes than are expected. Just stop
// listening, we'll fail below.
break;
}
}
// Now verify that the changes we got were the ones that were expected.
assertEquals(expected, changes);
}
private DocIdPusher.Record newRecord(Path path) throws Exception {
return new DocIdPusher.Record.Builder(delegate.newDocId(path))
.setCrawlImmediately(true).build();
}
private static Netapi32Ex.DFS_INFO_3 newDfsInfo3(Storage... storages) {
final int sizeOfInfo = new Netapi32Ex.DFS_STORAGE_INFO().size();
final int numberOfStorages = storages.length;
// Cannot supply length of 0 so always allocate 1 more byte than needed.
Memory storagesMem = new Memory(1 + numberOfStorages * sizeOfInfo);
int offset = 0;
for (Storage storage : storages) {
storagesMem.setLong(offset, storage.state);
offset += Native.LONG_SIZE;
writeWString(storagesMem, offset, storage.serverName);
offset += Pointer.SIZE;
writeWString(storagesMem, offset, storage.shareName);
offset += Pointer.SIZE;
}
Memory ptr = new Memory(40);
writeWString(ptr, 0, new WString(""));
writeWString(ptr, Pointer.SIZE, new WString(""));
ptr.setLong(2 * Pointer.SIZE, 0);
ptr.setLong(2 * Pointer.SIZE + Native.LONG_SIZE, numberOfStorages);
ptr.setPointer(2 * Pointer.SIZE + 2 * Native.LONG_SIZE, storagesMem);
return new Netapi32Ex.DFS_INFO_3(ptr);
}
private static void writeWString(Memory m, int offset, WString str) {
int len = (str.length() + 1) * Native.WCHAR_SIZE;
Memory ptr = new Memory(len);
ptr.setString(0, str);
m.setPointer(offset, ptr);
}
static class Storage {
protected final int state;
protected final WString serverName;
protected final WString shareName;
public Storage(int state, String serverName, String shareName) {
this.state = state;
this.serverName = new WString(serverName);
this.shareName = new WString(shareName);
}
}
}