/* * Copyright (C) 2016 Per Lundqvist * * 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 3 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, see <http://www.gnu.org/licenses/>. */ package com.github.perlundq.yajsync.internal.session; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.Charset; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.GroupPrincipal; import java.nio.file.attribute.UserPrincipal; import java.nio.file.attribute.UserPrincipalLookupService; import java.nio.file.attribute.UserPrincipalNotFoundException; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.github.perlundq.yajsync.attr.Group; import com.github.perlundq.yajsync.attr.RsyncFileAttributes; import com.github.perlundq.yajsync.attr.User; import com.github.perlundq.yajsync.internal.text.Text; import com.github.perlundq.yajsync.internal.util.Environment; import com.github.perlundq.yajsync.internal.util.Pair; public final class UnixFileAttributeManager extends FileAttributeManager { private static final Pattern ENTRY_PATTERN = Pattern.compile("^([^:]+):[^:]+:(\\d+):.*$"); private final Map<Integer, String> _userIdToUserName; private final Map<Integer, String> _groupIdToGroupName; private final Map<String, UserPrincipal> _nameToUserPrincipal; private final Map<String, GroupPrincipal> _nameToGroupPrincipal; private final User _defaultUser; private final Group _defaultGroup; private final boolean _isCacheEnabled; public UnixFileAttributeManager(User defaultUser, Group defaultGroup, boolean isPreserveUser, boolean isPreserveGroup) throws IOException { _defaultUser = defaultUser; _defaultGroup = defaultGroup; Pair<Map<Integer, String>, Map<Integer, String>> resOrNull = getUserAndGroupCaches(); _isCacheEnabled = resOrNull != null; if (_isCacheEnabled) { _userIdToUserName = resOrNull.first(); _groupIdToGroupName = resOrNull.second(); if (isPreserveUser) { _nameToUserPrincipal = userPrincipalsOf(_userIdToUserName.values()); } else { _nameToUserPrincipal = Collections.emptyMap(); } if (isPreserveGroup) { _nameToGroupPrincipal = groupPrincipalsOf(_groupIdToGroupName.values()); } else { _nameToGroupPrincipal = Collections.emptyMap(); } } else { _userIdToUserName = null; _groupIdToGroupName = null; _nameToUserPrincipal = null; _nameToGroupPrincipal = null; } } private static boolean isNssAvailable() { return Files.isReadable(Paths.get("/etc/nsswitch.conf")) && Environment.isExecutable("getent"); } private static Pair<Map<Integer, String>, Map<Integer, String>> getUserAndGroupCaches() throws IOException { Map<Integer, String> userIdToUserName; Map<Integer, String> groupIdToGroupName; try { if (isNssAvailable()) { if (!Environment.IS_FORK_ALLOWED) { throw new IOException("fork has been disabled"); } userIdToUserName = getNssEntries("passwd"); groupIdToGroupName = getNssEntries("group"); } else { userIdToUserName = readPasswdOrGroupFile("/etc/passwd"); groupIdToGroupName = readPasswdOrGroupFile("/etc/group"); } } catch (IOException e) { if (canStatOwnerAndGroup(Paths.get(Text.DOT))) { return null; } else { throw e; } } return new Pair<>(userIdToUserName, groupIdToGroupName); } private static boolean canStatOwnerAndGroup(Path path) { try { Files.readAttributes(path, "unix:owner,group", LinkOption.NOFOLLOW_LINKS); return true; } catch (IOException | UnsupportedOperationException e) { return false; } } private static Map<Integer, String> getEntries(BufferedReader reader) throws IOException { Map<Integer, String> idToName = new HashMap<>(); while (true) { String line = reader.readLine(); if (line == null) { return idToName; } Matcher m = ENTRY_PATTERN.matcher(line); if (m.matches()) { String name = m.group(1); int id = Integer.parseInt(m.group(2)); idToName.put(id, name); } } } private static Map<Integer, String> readPasswdOrGroupFile(String passwdOrGroup) throws IOException { try (BufferedReader reader = Files.newBufferedReader(Paths.get(passwdOrGroup), Charset.defaultCharset())) { return getEntries(reader); } } private static Map<Integer, String> getNssEntries(String passwdOrGroup) throws IOException { Process p = new ProcessBuilder("getent", passwdOrGroup).start(); try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()))) { return getEntries(r); } } private static Map<String, UserPrincipal> userPrincipalsOf(Collection<String> userNames) { UserPrincipalLookupService service = FileSystems.getDefault().getUserPrincipalLookupService(); Map<String, UserPrincipal> res = new HashMap<>(userNames.size()); for (String userName : userNames) { try { UserPrincipal principal = service.lookupPrincipalByName(userName); res.put(userName, principal); } catch (UserPrincipalNotFoundException e) { // ignored } catch (IOException e) { return res; } } return res; } private static Map<String, GroupPrincipal> groupPrincipalsOf(Collection<String> groupNames) { UserPrincipalLookupService service = FileSystems.getDefault().getUserPrincipalLookupService(); Map<String, GroupPrincipal> res = new HashMap<>(groupNames.size()); for (String groupName : groupNames) { try { GroupPrincipal principal = service.lookupPrincipalByGroupName(groupName); res.put(groupName, principal); } catch (UserPrincipalNotFoundException e) { // ignored } catch (IOException e) { return res; } } return res; } private RsyncFileAttributes cachedStat(Path path) throws IOException { String toStat = "unix:mode,lastModifiedTime,size,uid,gid"; Map<String, Object> attrs = Files.readAttributes(path, toStat, LinkOption.NOFOLLOW_LINKS); int mode = (int) attrs.get("mode"); long mtime = ((FileTime) attrs.get("lastModifiedTime")).to(TimeUnit.SECONDS); long size = (long) attrs.get("size"); int uid = (int) attrs.get("uid"); int gid = (int) attrs.get("gid"); String userName = _userIdToUserName.getOrDefault(uid, _defaultUser.name()); String groupName = _groupIdToGroupName.getOrDefault(gid, _defaultGroup.name()); User user = new User(userName, uid); Group group = new Group(groupName, gid); return new RsyncFileAttributes(mode, size, mtime, user, group); } private RsyncFileAttributes fullStat(Path path) throws IOException { String toStat = "unix:mode,lastModifiedTime,size,uid,gid,owner,group"; Map<String, Object> attrs = Files.readAttributes(path, toStat, LinkOption.NOFOLLOW_LINKS); int mode = (int) attrs.get("mode"); long mtime = ((FileTime) attrs.get("lastModifiedTime")).to(TimeUnit.SECONDS); long size = (long) attrs.get("size"); int uid = (int) attrs.get("uid"); int gid = (int) attrs.get("gid"); String userName = ((UserPrincipal ) attrs.get("owner")).getName(); String groupName = ((GroupPrincipal) attrs.get("group")).getName(); User user = new User(userName, uid); Group group = new Group(groupName, gid); return new RsyncFileAttributes(mode, size, mtime, user, group); } @Override public RsyncFileAttributes stat(Path path) throws IOException { if (_isCacheEnabled) { return cachedStat(path); } return fullStat(path); } @Override public void setFileMode(Path path, int mode, LinkOption... linkOption) throws IOException { Files.setAttribute(path, "unix:mode", mode, linkOption); } private UserPrincipal getUserPrincipalFrom(String userName) throws IOException { try { if (_isCacheEnabled) { return _nameToUserPrincipal.get(userName); } UserPrincipalLookupService service = FileSystems.getDefault().getUserPrincipalLookupService(); return service.lookupPrincipalByName(userName); } catch (IOException | UnsupportedOperationException e) { return null; } } private GroupPrincipal getGroupPrincipalFrom(String groupName) throws IOException { try { if (_isCacheEnabled) { return _nameToGroupPrincipal.get(groupName); } UserPrincipalLookupService service = FileSystems.getDefault().getUserPrincipalLookupService(); return service.lookupPrincipalByGroupName(groupName); } catch (IOException | UnsupportedOperationException e) { return null; } } @Override public void setOwner(Path path, User user, LinkOption... linkOption) throws IOException { UserPrincipal principal = getUserPrincipalFrom(user.name()); if (principal == null) { setUserId(path, user.id(), linkOption); } Files.setAttribute(path, "unix:owner", principal, linkOption); } @Override public void setGroup(Path path, Group group, LinkOption... linkOption) throws IOException { GroupPrincipal principal = getGroupPrincipalFrom(group.name()); if (principal == null) { setGroupId(path, group.id(), linkOption); } Files.setAttribute(path, "unix:group", principal, linkOption); } @Override public void setUserId(Path path, int uid, LinkOption... linkOption) throws IOException { Files.setAttribute(path, "unix:uid", uid, linkOption); } @Override public void setGroupId(Path path, int gid, LinkOption... linkOption) throws IOException { Files.setAttribute(path, "unix:gid", gid, linkOption); } }