package net.pms.util; import com.ibm.icu.text.CharsetDetector; import com.ibm.icu.text.CharsetMatch; import com.sun.jna.Platform; import java.io.*; import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.StandardCharsets; import java.nio.file.FileStore; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.PosixFileAttributes; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.pms.PMS; import net.pms.configuration.PmsConfiguration; import net.pms.dlna.DLNAMediaInfo; import net.pms.dlna.DLNAMediaSubtitle; import net.pms.formats.FormatFactory; import net.pms.formats.v2.SubtitleType; import static net.pms.util.Constants.*; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.WordUtils; import static org.apache.commons.lang3.StringUtils.*; import org.codehaus.plexus.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FileUtil { private static final Logger LOGGER = LoggerFactory.getLogger(FileUtil.class); private static final ReentrantLock subtitleCacheLock = new ReentrantLock(); private static final Map<File, File[]> subtitleCache = new HashMap<>(); private static final int S_ISVTX = 512; // Unix sticky bit mask // Signal an invalid parameter in getFileLocation() without raising an exception or returning null private static final String DEFAULT_BASENAME = "NO_DEFAULT_BASENAME_SUPPLIED.conf"; // This class is not instantiable private FileUtil() { } /** * A helper class used by {@link #getFileLocation(String, String, String)} * which provides access to a file's absolute path and that of its directory. * * @since 1.90.0 */ public static final class FileLocation { private String directoryPath; private String filePath; FileLocation(File directory, File file) { this.directoryPath = FilenameUtils.normalize(directory.getAbsolutePath()); this.filePath = FilenameUtils.normalize(file.getAbsolutePath()); } public String getDirectoryPath() { return directoryPath; } public String getFilePath() { return filePath; } } /** * Returns a {@link FileLocation} object which provides access to the directory * and file paths of the specified file as normalised, absolute paths. * * This determines the directory and file path of a file according to the rules * outlined here: http://www.ps3mediaserver.org/forum/viewtopic.php?f=6&t=3507&p=49895#p49895 * * @param customPath an optional user-defined path for the resource * @param defaultDirectory a default directory path used if no custom path is provided * @param defaultBasename a default filename used if a) no custom path is provided * or b) the custom path is a directory * @return a {@link FileLocation} object providing access to the file's directory and file paths * @since 1.90.0 */ // this is called from a static initialiser, where errors aren't clearly reported, // so do everything possible to return a valid reponse, even if the parameters // aren't sane static public FileLocation getFileLocation( String customPath, String defaultDirectory, String defaultBasename ) { File customFile = null; File directory = null; File file = null; if (isBlank(defaultBasename)) { // shouldn't get here defaultBasename = DEFAULT_BASENAME; } if (defaultDirectory == null) { defaultDirectory = ""; // current directory } if (customPath != null) { customFile = new File(customPath).getAbsoluteFile(); } if (customFile != null) { if (customFile.exists()) { if (customFile.isDirectory()) { directory = customFile; file = new File(customFile, defaultBasename).getAbsoluteFile(); } else { directory = customFile.getParentFile(); file = customFile; } } else { File parentDirectoryFile = customFile.getParentFile(); if (parentDirectoryFile != null && parentDirectoryFile.exists()) { // parent directory exists: the file can be created directory = parentDirectoryFile; file = customFile; } } } if (directory == null || file == null) { directory = new File(defaultDirectory).getAbsoluteFile(); file = new File(directory, defaultBasename).getAbsoluteFile(); } return new FileLocation(directory, file); } public final static class InvalidFileSystemException extends Exception { private static final long serialVersionUID = -4545843729375389876L; public InvalidFileSystemException() { super(); } public InvalidFileSystemException(String message) { super(message); } public InvalidFileSystemException(Throwable cause) { super(cause); } public InvalidFileSystemException(String message, Throwable cause) { super(message, cause); } } /** * A simple type holding mount point information for Unix file systems. * * @author Nadahar */ public static final class UnixMountPoint { public String device; public String folder; @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (!(obj instanceof UnixMountPoint)) { return false; } return this.device.equals(((UnixMountPoint) obj).device) && this.folder.equals(((UnixMountPoint) obj).folder); } @Override public int hashCode() { return device.hashCode() + folder.hashCode(); } @Override public String toString() { return String.format("Device: \"%s\", folder: \"%s\"", device, folder); } } public static File isFileExists(String f, String ext) { return isFileExists(new File(f), ext); } public static boolean isUrl(String filename) { // We're intentionally avoiding stricter URI() methods, which can throw // URISyntaxException for psuedo-urls (e.g. librtmp-style urls containing spaces) return filename != null && filename.matches("\\S+://.*"); } public static String getProtocol(String filename) { // Intentionally avoids URI.getScheme(), see above if (isUrl(filename)) { return filename.split("://")[0].toLowerCase(); } return null; } public static String urlJoin(String base, String filename) { if (isUrl(filename)) { return filename; } try { return new URL(new URL(base), filename).toString(); } catch (MalformedURLException e) { return filename; } } public static String getUrlExtension(String u) { // Omit the query string, if any return getExtension(substringBefore(u, "?")); } public static String getExtension(File f) { if (f == null || f.getName() == null) { return null; } return getExtension(f.getName()); } public static String getExtension(String f) { int point = f.lastIndexOf('.'); if (point == -1) { return null; } return f.substring(point + 1); } public static String getFileNameWithoutExtension(String f) { int point = f.lastIndexOf('.'); if (point == -1) { point = f.length(); } return f.substring(0, point); } private static final class FormattedNameAndEdition { public String formattedName; public String edition; public FormattedNameAndEdition(String formattedName, String edition) { this.formattedName = formattedName; this.edition = edition; } } /** * Remove and save edition information to be added later */ private static FormattedNameAndEdition removeAndSaveEditionToBeAddedLater(String formattedName) { String edition = null; Matcher m = COMMON_FILE_EDITIONS_PATTERN.matcher(formattedName); if (m.find()) { edition = m.group().replaceAll("\\.", " "); edition = "(" + WordUtils.capitalizeFully(edition) + ")"; formattedName = formattedName.replaceAll(" - " + COMMON_FILE_EDITIONS, ""); formattedName = formattedName.replaceAll(COMMON_FILE_EDITIONS, ""); } return new FormattedNameAndEdition(formattedName, edition); } /** * Capitalize the first letter of each word if the string contains no capital letters */ private static String convertFormattedNameToTitleCaseParts(String formattedName) { if (formattedName.equals(formattedName.toLowerCase())) { StringBuilder formattedNameBuilder = new StringBuilder(); for (String part : formattedName.split(" - ")) { if (formattedNameBuilder.length() > 0) { formattedNameBuilder.append(" - "); } formattedNameBuilder.append(convertLowerCaseStringToTitleCase(part)); } formattedName = formattedNameBuilder.toString(); } return formattedName; } /** * Capitalize the first letter of each word if the string contains no capital letters */ private static String convertFormattedNameToTitleCase(String formattedName) { if (formattedName.equals(formattedName.toLowerCase())) { formattedName = convertLowerCaseStringToTitleCase(formattedName); } return formattedName; } /** * Remove group name from the beginning of the filename * * @param fileNameWithoutExtension */ private static String removeGroupNameFromBeginning(String formattedName) { if (!"".equals(formattedName) && formattedName.startsWith("[")) { Pattern pattern = Pattern.compile("^\\[[^\\]]{0,20}\\][^\\w]*(\\w.*?)\\s*$"); Matcher matcher = pattern.matcher(formattedName); if (matcher.find()) { formattedName = matcher.group(1); } else if (formattedName.endsWith("]")) { pattern = Pattern.compile("^\\[([^\\[\\]]+)\\]\\s*$"); matcher = pattern.matcher(formattedName); if (matcher.find()) { formattedName = matcher.group(1); } } } return formattedName; } /** * Remove stuff at the end of the filename like release group, quality, source, etc. */ private static String removeFilenameEndMetadata(String formattedName) { formattedName = formattedName.replaceAll(COMMON_FILE_ENDS_CASE_SENSITIVE, ""); formattedName = formattedName.replaceAll("(?i)" + COMMON_FILE_ENDS, ""); return formattedName; } /** * Strings that only occur after all useful information. * When we encounter one of these strings, the string and everything after * them will be removed. */ private static final String COMMON_FILE_ENDS = "[\\s\\.]AC3.*|[\\s\\.]REPACK.*|[\\s\\.]480p.*|[\\s\\.]720p.*|[\\s\\.]m-720p.*|[\\s\\.]900p.*|[\\s\\.]1080p.*|[\\s\\.]2160p.*|[\\s\\.]WEB-DL.*|[\\s\\.]HDTV.*|[\\s\\.]DSR.*|[\\s\\.]PDTV.*|[\\s\\.]WS.*|[\\s\\.]HQ.*|[\\s\\.]DVDRip.*|[\\s\\.]TVRiP.*|[\\s\\.]BDRip.*|[\\s\\.]BRRip.*|[\\s\\.]WEBRip.*|[\\s\\.]BluRay.*|[\\s\\.]Blu-ray.*|[\\s\\.]SUBBED.*|[\\s\\.]x264.*|[\\s\\.]Dual[\\s\\.]Audio.*|[\\s\\.]HSBS.*|[\\s\\.]H-SBS.*|[\\s\\.]RERiP.*|[\\s\\.]DIRFIX.*|[\\s\\.]READNFO.*|[\\s\\.]60FPS.*"; private static final String COMMON_FILE_ENDS_MATCH = ".*[\\s\\.]AC3.*|.*[\\s\\.]REPACK.*|.*[\\s\\.]480p.*|.*[\\s\\.]720p.*|.*[\\s\\.]m-720p.*|.*[\\s\\.]900p.*|.*[\\s\\.]1080p.*|.*[\\s\\.]2160p.*|.*[\\s\\.]WEB-DL.*|.*[\\s\\.]HDTV.*|.*[\\s\\.]DSR.*|.*[\\s\\.]PDTV.*|.*[\\s\\.]WS.*|.*[\\s\\.]HQ.*|.*[\\s\\.]DVDRip.*|.*[\\s\\.]TVRiP.*|.*[\\s\\.]BDRip.*|.*[\\s\\.]BRRip.*|.*[\\s\\.]WEBRip.*|.*[\\s\\.]BluRay.*|.*[\\s\\.]Blu-ray.*|.*[\\s\\.]SUBBED.*|.*[\\s\\.]x264.*|.*[\\s\\.]Dual[\\s\\.]Audio.*|.*[\\s\\.]HSBS.*|.*[\\s\\.]H-SBS.*|.*[\\s\\.]RERiP.*|.*[\\s\\.]DIRFIX.*|.*[\\s\\.]READNFO.*|.*[\\s\\.]60FPS.*"; /** * Same as above, but they are common words so we reduce the chances of a * false-positive by being case-sensitive. */ private static final String COMMON_FILE_ENDS_CASE_SENSITIVE = "[\\s\\.]PROPER[\\s\\.].*|[\\s\\.]iNTERNAL[\\s\\.].*|[\\s\\.]LIMITED[\\s\\.].*|[\\s\\.]LiMiTED[\\s\\.].*|[\\s\\.]FESTiVAL[\\s\\.].*|[\\s\\.]NORDIC[\\s\\.].*|[\\s\\.]REAL[\\s\\.].*|[\\s\\.]SUBBED[\\s\\.].*|[\\s\\.]RETAIL[\\s\\.].*|[\\s\\.]EXTENDED[\\s\\.].*|[\\s\\.]NEWEDIT[\\s\\.].*|[\\s\\.]WEB[\\s\\.].*"; /** * Editions to be added to the end of the prettified name */ private static final String COMMON_FILE_EDITIONS = "(?i)(?!\\()(Special[\\s\\.]Edition|Unrated|Final[\\s\\.]Cut|Remastered|Extended[\\s\\.]Cut|IMAX[\\s\\.]Edition|Uncensored|Directors[\\s\\.]Cut|Uncut)(?!\\))"; private static final Pattern COMMON_FILE_EDITIONS_PATTERN = Pattern.compile(COMMON_FILE_EDITIONS); /** * Returns the filename after being "prettified", which involves * attempting to strip away certain things like information about the * quality, resolution, codecs, release groups, fansubbers, etc., * replacing periods with spaces, and various other things to produce a * more "pretty" and standardized filename. * * @param f The filename * @param file The file to possibly be used by the InfoDb * * @return The prettified filename */ public static String getFileNamePrettified(String f, File file) { String fileNameWithoutExtension; String formattedName = ""; String formattedNameTemp; String searchFormattedName; String edition = ""; // These are false unless we recognize that we could use some info on the video from IMDb boolean isEpisodeToLookup = false; boolean isTVSeriesToLookup = false; boolean isMovieToLookup = false; boolean isMovieWithoutYear = false; // Remove file extension fileNameWithoutExtension = getFileNameWithoutExtension(f); formattedName = removeGroupNameFromBeginning(fileNameWithoutExtension); searchFormattedName = ""; if (formattedName.matches(".*[sS]0\\d[eE]\\d\\d([eE]|-[eE])\\d\\d.*")) { // This matches scene and most p2p TV episodes within the first 9 seasons that are more than one episode isTVSeriesToLookup = true; // Rename the season/episode numbers. For example, "S01E01" changes to " - 101" // Then strip the end of the episode if it does not have the episode name in the title formattedName = formattedName.replaceAll("(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)([eE]|-[eE])(\\d)(\\d)(" + COMMON_FILE_ENDS + ")", " - $1$2$3-$5$6"); formattedName = formattedName.replaceAll("[\\s\\.]S0(\\d)E(\\d)(\\d)([eE]|-[eE])(\\d)(\\d)(" + COMMON_FILE_ENDS_CASE_SENSITIVE + ")", " - $1$2$3-$5$6"); FormattedNameAndEdition result = removeAndSaveEditionToBeAddedLater(formattedName); formattedName = result.formattedName; if (result.edition != null) { edition = result.edition; } // If it matches this then it didn't match the previous one, which means there is probably an episode title in the filename formattedNameTemp = formattedName.replaceAll("(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)([eE]|-[eE])(\\d)(\\d)[\\s\\.]", " - $1$2$3-$5$6 - "); if (PMS.getConfiguration().isUseInfoFromIMDb() && formattedName.equals(formattedNameTemp)) { isEpisodeToLookup = true; } formattedName = formattedNameTemp; formattedName = removeFilenameEndMetadata(formattedName); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); formattedName = convertFormattedNameToTitleCaseParts(formattedName); } else if (formattedName.matches(".*[sS][1-9]\\d[eE]\\d\\d([eE]|-[eE])\\d\\d.*")) { // This matches scene and most p2p TV episodes after their first 9 seasons that are more than one episode isTVSeriesToLookup = true; // Rename the season/episode numbers. For example, "S11E01" changes to " - 1101" formattedName = formattedName.replaceAll("(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)([eE]|-[eE])(\\d)(\\d)(" + COMMON_FILE_ENDS + ")", " - $1$2$3-$5$6"); formattedName = formattedName.replaceAll("[\\s\\.]S([1-9]\\d)E(\\d)(\\d)([eE]|-[eE])(\\d)(\\d)(" + COMMON_FILE_ENDS_CASE_SENSITIVE + ")", " - $1$2$3-$5$6"); FormattedNameAndEdition result = removeAndSaveEditionToBeAddedLater(formattedName); formattedName = result.formattedName; if (result.edition != null) { edition = result.edition; } // If it matches this then it didn't match the previous one, which means there is probably an episode title in the filename formattedNameTemp = formattedName.replaceAll("(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)([eE]|-[eE])(\\d)(\\d)[\\s\\.]", " - $1$2$3-$5$6 - "); if (PMS.getConfiguration().isUseInfoFromIMDb() && formattedName.equals(formattedNameTemp)) { isEpisodeToLookup = true; } formattedName = formattedNameTemp; formattedName = removeFilenameEndMetadata(formattedName); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); formattedName = convertFormattedNameToTitleCaseParts(formattedName); } else if (formattedName.matches(".*[sS]0\\d[eE]\\d\\d.*")) { // This matches scene and most p2p TV episodes within the first 9 seasons isTVSeriesToLookup = true; FormattedNameAndEdition result = removeAndSaveEditionToBeAddedLater(formattedName); formattedName = result.formattedName; if (result.edition != null) { edition = result.edition; } // Rename the season/episode numbers. For example, "S01E01" changes to " - 101" // Then strip the end of the episode if it does not have the episode name in the title formattedName = formattedName.replaceAll("(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)(" + COMMON_FILE_ENDS + ")", " - $1$2$3"); formattedName = formattedName.replaceAll("[\\s\\.]S0(\\d)E(\\d)(\\d)(" + COMMON_FILE_ENDS_CASE_SENSITIVE + ")", " - $1$2$3"); // If it matches this then it didn't match the previous one, which means there is probably an episode title in the filename formattedNameTemp = formattedName.replaceAll("(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)[\\s\\.]", " - $1$2$3 - "); if (PMS.getConfiguration().isUseInfoFromIMDb() && formattedName.equals(formattedNameTemp)) { isEpisodeToLookup = true; } formattedName = formattedNameTemp; formattedName = removeFilenameEndMetadata(formattedName); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); formattedName = convertFormattedNameToTitleCaseParts(formattedName); } else if (formattedName.matches(".*[sS][1-9]\\d[eE]\\d\\d.*")) { // This matches scene and most p2p TV episodes after their first 9 seasons isTVSeriesToLookup = true; // Rename the season/episode numbers. For example, "S11E01" changes to " - 1101" formattedName = formattedName.replaceAll("(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)(" + COMMON_FILE_ENDS + ")", " - $1$2$3"); formattedName = formattedName.replaceAll("[\\s\\.]S([1-9]\\d)E(\\d)(\\d)(" + COMMON_FILE_ENDS_CASE_SENSITIVE + ")", " - $1$2$3"); FormattedNameAndEdition result = removeAndSaveEditionToBeAddedLater(formattedName); formattedName = result.formattedName; if (result.edition != null) { edition = result.edition; } // If it matches this then it didn't match the previous one, which means there is probably an episode title in the filename formattedNameTemp = formattedName.replaceAll("(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)[\\s\\.]", " - $1$2$3 - "); if (PMS.getConfiguration().isUseInfoFromIMDb() && formattedName.equals(formattedNameTemp)) { isEpisodeToLookup = true; } formattedName = formattedNameTemp; formattedName = removeFilenameEndMetadata(formattedName); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); formattedName = convertFormattedNameToTitleCaseParts(formattedName); } else if (formattedName.matches(".*[\\s\\.](19|20)\\d\\d[\\s\\.][0-1]\\d[\\s\\.][0-3]\\d[\\s\\.].*")) { // This matches scene and most p2p TV episodes that release several times per week isTVSeriesToLookup = true; // Rename the date. For example, "2013.03.18" changes to " - 2013/03/18" formattedName = formattedName.replaceAll("(?i)[\\s\\.](19|20)(\\d\\d)[\\s\\.]([0-1]\\d)[\\s\\.]([0-3]\\d)(" + COMMON_FILE_ENDS + ")", " - $1$2/$3/$4"); formattedName = formattedName.replaceAll("[\\s\\.](19|20)(\\d\\d)[\\s\\.]([0-1]\\d)[\\s\\.]([0-3]\\d)(" + COMMON_FILE_ENDS_CASE_SENSITIVE + ")", " - $1$2/$3/$4"); FormattedNameAndEdition result = removeAndSaveEditionToBeAddedLater(formattedName); formattedName = result.formattedName; if (result.edition != null) { edition = result.edition; } // If it matches this then it didn't match the previous one, which means there is probably an episode title in the filename formattedNameTemp = formattedName.replaceAll("(?i)[\\s\\.](19|20)(\\d\\d)[\\s\\.]([0-1]\\d)[\\s\\.]([0-3]\\d)[\\s\\.]", " - $1$2/$3/$4 - "); if (PMS.getConfiguration().isUseInfoFromIMDb() && formattedName.equals(formattedNameTemp)) { isEpisodeToLookup = true; } formattedName = formattedNameTemp; formattedName = removeFilenameEndMetadata(formattedName); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); formattedName = convertFormattedNameToTitleCaseParts(formattedName); } else if (formattedName.matches(".*[\\s\\.](19|20)\\d\\d[\\s\\.].*")) { // This matches scene and most p2p movies isMovieToLookup = true; // Rename the year. For example, "2013" changes to " (2013)" formattedName = formattedName.replaceAll("[\\s\\.](19|20)(\\d\\d)", " ($1$2)"); formattedName = removeFilenameEndMetadata(formattedName); FormattedNameAndEdition result = removeAndSaveEditionToBeAddedLater(formattedName); formattedName = result.formattedName; if (result.edition != null) { edition = result.edition; } // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); formattedName = convertFormattedNameToTitleCase(formattedName); } else if (formattedName.matches(".*\\[(19|20)\\d\\d\\].*")) { // This matches rarer types of movies isMovieToLookup = true; // Rename the year. For example, "2013" changes to " (2013)" formattedName = formattedName.replaceAll("(?i)\\[(19|20)(\\d\\d)\\].*", " ($1$2)"); formattedName = removeFilenameEndMetadata(formattedName); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); formattedName = convertFormattedNameToTitleCase(formattedName); } else if (formattedName.matches(".*\\((19|20)\\d\\d\\).*")) { // This matches rarer types of movies isMovieToLookup = true; formattedName = removeFilenameEndMetadata(formattedName); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); formattedName = convertFormattedNameToTitleCase(formattedName); } else if (formattedName.matches(".*\\[[0-9a-zA-Z]{8}\\]$")) { // This matches anime with a hash at the end of the name isTVSeriesToLookup = true; // Remove underscores formattedName = formattedName.replaceAll("_", " "); // Remove stuff at the end of the filename like hash, quality, source, etc. formattedName = formattedName.replaceAll("(?i)\\s\\(1280x720.*|\\s\\(1920x1080.*|\\s\\(720x400.*|\\[720p.*|\\[1080p.*|\\[480p.*|\\s\\(BD.*|\\s\\[Blu-Ray.*|\\s\\[DVD.*|\\.DVD.*|\\[[0-9a-zA-Z]{8}\\]$|\\[h264.*|R1DVD.*|\\[BD.*", ""); if (PMS.getConfiguration().isUseInfoFromIMDb() && formattedName.substring(formattedName.length() - 3).matches("[\\s\\._]\\d\\d")) { isEpisodeToLookup = true; searchFormattedName = formattedName.substring(0, formattedName.length() - 2) + "S01E" + formattedName.substring(formattedName.length() - 2); } formattedName = convertFormattedNameToTitleCase(formattedName); } else if (formattedName.matches(".*\\[BD\\].*|.*\\[720p\\].*|.*\\[1080p\\].*|.*\\[480p\\].*|.*\\[Blu-Ray.*|.*\\[h264.*")) { // This matches anime without a hash in the name isTVSeriesToLookup = true; // Remove underscores formattedName = formattedName.replaceAll("_", " "); // Remove stuff at the end of the filename like hash, quality, source, etc. formattedName = formattedName.replaceAll("(?i)\\[BD\\].*|\\[720p.*|\\[1080p.*|\\[480p.*|\\[Blu-Ray.*|\\[h264.*", ""); if (PMS.getConfiguration().isUseInfoFromIMDb() && formattedName.substring(formattedName.length() - 3).matches("[\\s\\._]\\d\\d")) { isEpisodeToLookup = true; searchFormattedName = formattedName.substring(0, formattedName.length() - 2) + "S01E" + formattedName.substring(formattedName.length() - 2); } formattedName = convertFormattedNameToTitleCase(formattedName); } else if (formattedName.matches(COMMON_FILE_ENDS_MATCH)) { // This is probably a movie that doesn't specify a year isMovieToLookup = true; isMovieWithoutYear = true; formattedName = removeFilenameEndMetadata(formattedName); FormattedNameAndEdition result = removeAndSaveEditionToBeAddedLater(formattedName); formattedName = result.formattedName; if (result.edition != null) { edition = result.edition; } // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); formattedName = convertFormattedNameToTitleCase(formattedName); } // Remove extra spaces formattedName = formattedName.replaceAll("\\s+", " "); /** * Add info from IMDb * * We use the Jaro Winkler similarity algorithm to make sure that changes to * movie or TV show names are only made when the difference between the * original and replacement names is less than 10%. * This means we get proper case and special characters without worrying about * incorrect results being used. * * TODO: Make the following logic only happen once. */ if (file != null && (isTVSeriesToLookup || isMovieToLookup)) { InfoDb.InfoDbData info = PMS.get().infoDb().get(file); if (info == null) { PMS.get().infoDbAdd(file, StringUtil.hasValue(searchFormattedName) ? searchFormattedName : formattedName); } else if (isTVSeriesToLookup) { int showNameIndex = indexOf(Pattern.compile("(?i) - \\d\\d\\d.*"), formattedName); if (StringUtils.isNotEmpty(info.title) && showNameIndex != -1) { String titleFromFilename = formattedName.substring(0, showNameIndex); // The following line can run over 100 times in under 1ms double similarity = org.apache.commons.lang3.StringUtils.getJaroWinklerDistance(titleFromFilename, info.title); if (similarity > 0.91) { formattedName = info.title + formattedName.substring(showNameIndex); if (isEpisodeToLookup) { if (StringUtils.isNotEmpty(info.ep_name)) { formattedName += " - " + info.ep_name; } } } LOGGER.trace("The similarity between '" + info.title + "' and '" + titleFromFilename + "' is " + similarity); } } else if (isMovieToLookup && StringUtils.isNotEmpty(info.title) && StringUtils.isNotEmpty(info.year)) { double similarity; if (isMovieWithoutYear) { similarity = org.apache.commons.lang3.StringUtils.getJaroWinklerDistance(formattedName, info.title); LOGGER.trace("The similarity between '" + info.title + "' and '" + formattedName + "' is " + similarity); } else { int yearIndex = indexOf(Pattern.compile("\\s\\(\\d{4}\\)"), formattedName); String titleFromFilename = formattedName.substring(0, yearIndex); similarity = org.apache.commons.lang3.StringUtils.getJaroWinklerDistance(titleFromFilename, info.title); LOGGER.trace("The similarity between '" + info.title + "' and '" + titleFromFilename + "' is " + similarity); } if (similarity > 0.91) { formattedName = info.title + " (" + info.year + ")"; } } } formattedName = formattedName.trim(); // Add the edition information if it exists if (!edition.isEmpty()) { String substr = formattedName.substring(Math.max(0, formattedName.length() - 2)); if (" -".equals(substr)) { formattedName = formattedName.substring(0, formattedName.length() - 2); } formattedName += " " + edition; } return formattedName; } /** * Converts a lower case string to title case. * * It is not very smart right now so it can be expanded to be more reliable. * * @param value the string to convert * * @return the converted string */ public static String convertLowerCaseStringToTitleCase(String value) { String convertedValue = ""; boolean loopedOnce = false; for (String word : value.split(" ")) { if (loopedOnce) { switch (word) { case "a": case "an": case "and": case "in": case "it": case "for": case "of": case "on": case "the": case "to": case "vs": convertedValue += ' ' + word; break; default: convertedValue += ' ' + word.substring(0, 1).toUpperCase() + word.substring(1); } } else { // Always capitalize the first letter of the string convertedValue += word.substring(0, 1).toUpperCase() + word.substring(1); } loopedOnce = true; } return convertedValue; } public static int indexOf(Pattern pattern, String s) { Matcher matcher = pattern.matcher(s); return matcher.find() ? matcher.start() : -1; } public static File getFileNameWithNewExtension(File parent, File file, String ext) { return isFileExists(new File(parent, file.getName()), ext); } /** * @deprecated Use {@link #getFileNameWithNewExtension(File, File, String)}. */ @Deprecated public static File getFileNameWitNewExtension(File parent, File f, String ext) { return getFileNameWithNewExtension(parent, f, ext); } public static File getFileNameWithAddedExtension(File parent, File f, String ext) { File ff = new File(parent, f.getName() + ext); if (ff.exists()) { return ff; } return null; } /** * @deprecated Use {@link #getFileNameWithAddedExtension(File, File, String)}. */ @Deprecated public static File getFileNameWitAddedExtension(File parent, File file, String ext) { return getFileNameWithAddedExtension(parent, file, ext); } public static File isFileExists(File f, String ext) { int point = f.getName().lastIndexOf('.'); if (point == -1) { point = f.getName().length(); } File lowerCasedFile = new File(f.getParentFile(), f.getName().substring(0, point) + "." + ext.toLowerCase()); if (lowerCasedFile.exists()) { return lowerCasedFile; } File upperCasedFile = new File(f.getParentFile(), f.getName().substring(0, point) + "." + ext.toUpperCase()); if (upperCasedFile.exists()) { return upperCasedFile; } return null; } /** * @deprecated Use {@link #isSubtitlesExists(File file, DLNAMediaInfo media)} instead. */ @Deprecated public static boolean doesSubtitlesExists(File file, DLNAMediaInfo media) { return isSubtitlesExists(file, media); } public static boolean isSubtitlesExists(File file, DLNAMediaInfo media) { return isSubtitlesExists(file, media, true); } /** * @deprecated Use {@link #isSubtitlesExists(File file, DLNAMediaInfo media, boolean usecache)} instead. */ @Deprecated public static boolean doesSubtitlesExists(File file, DLNAMediaInfo media, boolean usecache) { return isSubtitlesExists(file, media, usecache); } public static boolean isSubtitlesExists(File file, DLNAMediaInfo media, boolean usecache) { if (media != null && media.isExternalSubsParsed()) { return media.isExternalSubsExist(); } boolean found = false; if (file.exists()) { found = browseFolderForSubtitles(file.getParentFile(), file, media, usecache); } String alternate = PMS.getConfiguration().getAlternateSubtitlesFolder(); if (isNotBlank(alternate)) { // https://code.google.com/p/ps3mediaserver/issues/detail?id=737#c5 File subFolder = new File(alternate); if (!subFolder.isAbsolute()) { subFolder = new File(file.getParent(), alternate); try { subFolder = subFolder.getCanonicalFile(); } catch (IOException e) { LOGGER.warn("Could not resolve alternative subtitles folder: {}", e.getMessage()); LOGGER.trace("", e); } } if (subFolder.exists()) { found = browseFolderForSubtitles(subFolder, file, media, usecache) || found; } } if (media != null) { media.setExternalSubsExist(found); media.setExternalSubsParsed(true); } return found; } private static boolean browseFolderForSubtitles(File subFolder, File file, DLNAMediaInfo media, final boolean useCache) { boolean found = false; final Set<String> supported = SubtitleType.getSupportedFileExtensions(); File[] allSubs = null; // TODO This caching scheme is very restrictive locking the whole cache // while populating a single folder. A more effective solution should // be implemented. subtitleCacheLock.lock(); try { if (useCache) { allSubs = subtitleCache.get(subFolder); } if (allSubs == null) { allSubs = subFolder.listFiles( new FilenameFilter() { @Override public boolean accept(File dir, String name) { String ext = FilenameUtils.getExtension(name).toLowerCase(); if ("sub".equals(ext)) { // Avoid microdvd/vobsub confusion by ignoring sub+idx pairs here since // they'll come in unambiguously as vobsub via the idx file anyway return isFileExists(new File(dir, name), "idx") == null; } return supported.contains(ext); } } ); if (allSubs != null) { subtitleCache.put(subFolder, allSubs); } } } finally { subtitleCacheLock.unlock(); } String fileName = getFileNameWithoutExtension(file.getName()).toLowerCase(); if (allSubs != null) { for (File f : allSubs) { if (f.isFile() && !f.isHidden()) { String fName = f.getName().toLowerCase(); for (String ext : supported) { if (fName.length() > ext.length() && fName.startsWith(fileName) && endsWithIgnoreCase(fName, "." + ext)) { int a = fileName.length(); int b = fName.length() - ext.length() - 1; String code = ""; if (a <= b) { // handling case with several dots: <video>..<extension> code = fName.substring(a, b); } if (code.startsWith(".")) { code = code.substring(1); } boolean exists = false; if (media != null) { for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) { if (f.equals(sub.getExternalFile())) { exists = true; } else if (equalsIgnoreCase(ext, "idx") && sub.getType() == SubtitleType.MICRODVD) { // sub+idx => VOBSUB sub.setType(SubtitleType.VOBSUB); exists = true; } else if (equalsIgnoreCase(ext, "sub") && sub.getType() == SubtitleType.VOBSUB) { // VOBSUB try { sub.setExternalFile(f, null); } catch (FileNotFoundException ex) { LOGGER.warn("File not found during external subtitles scan: {}", ex.getMessage()); LOGGER.trace("", ex); } exists = true; } } } if (!exists) { String forcedLang = null; DLNAMediaSubtitle sub = new DLNAMediaSubtitle(); sub.setId(100 + (media == null ? 0 : media.getSubtitleTracksList().size())); // fake id, not used if (code.length() == 0 || !Iso639.codeIsValid(code)) { sub.setLang(DLNAMediaSubtitle.UND); sub.setType(SubtitleType.valueOfFileExtension(ext)); if (code.length() > 0) { sub.setSubtitlesTrackTitleFromMetadata(code); if (sub.getSubtitlesTrackTitleFromMetadata().contains("-")) { String flavorLang = sub.getSubtitlesTrackTitleFromMetadata().substring(0, sub.getSubtitlesTrackTitleFromMetadata().indexOf('-')); String flavorTitle = sub.getSubtitlesTrackTitleFromMetadata().substring(sub.getSubtitlesTrackTitleFromMetadata().indexOf('-') + 1); if (Iso639.codeIsValid(flavorLang)) { sub.setLang(flavorLang); sub.setSubtitlesTrackTitleFromMetadata(flavorTitle); forcedLang = flavorLang; } } } } else { sub.setLang(code); sub.setType(SubtitleType.valueOfFileExtension(ext)); forcedLang = code; } try { sub.setExternalFile(f, forcedLang); } catch (FileNotFoundException ex) { LOGGER.warn("File not found during external subtitles scan: {}", ex.getMessage()); LOGGER.trace("", ex); } found = true; if (media != null) { media.getSubtitleTracksList().add(sub); } } } } } } } return found; } /** * Detects charset/encoding for given file. Not 100% accurate for * non-Unicode files. * * @param file the file for which to detect charset/encoding * @return The match object form the detection process or <code>null</code> if no match was found * @throws IOException */ public static CharsetMatch getFileCharsetMatch(File file) throws IOException { InputStream in = new BufferedInputStream(new FileInputStream(file)); CharsetDetector detector = new CharsetDetector(); detector.setText(in); // Results are sorted on descending confidence, so we're only after the first one. return detector.detectAll()[0]; } /** * Detects charset/encoding for given file. Not 100% accurate for * non-Unicode files. * * @param file the file for which to detect charset/encoding * @return The detected <code>Charset</code> or <code>null</code> if not detected * @throws IOException */ public static Charset getFileCharset(File file) throws IOException { CharsetMatch match = getFileCharsetMatch(file); if (match != null) { try { if (Charset.isSupported(match.getName())) { LOGGER.debug("Detected charset \"{}\" in file {}", match.getName(), file.getAbsolutePath()); return Charset.forName(match.getName()); } else { LOGGER.debug( "Detected charset \"{}\" in file {}, but cannot use it because it's not supported by the Java Virual Machine", match.getName(), file.getAbsolutePath() ); return null; } } catch (IllegalCharsetNameException e) { LOGGER.debug("Illegal charset deteceted \"{}\" in file {}", match.getName(), file.getAbsolutePath()); } } LOGGER.debug("Found no matching charset for file {}", file.getAbsolutePath()); return null; } /** * Detects charset/encoding for given file. Not 100% accurate for * non-Unicode files. * * @param file the file for which to detect charset/encoding * @return The name of the detected charset or <code>null</code> if not detected * @throws IOException */ public static String getFileCharsetName(File file) throws IOException { CharsetMatch match = getFileCharsetMatch(file); if (match != null) { LOGGER.debug("Detected charset \"{}\" in file {}", match.getName(), file.getAbsolutePath()); return match.getName().toUpperCase(PMS.getLocale()); } else { LOGGER.debug("Found no matching charset for file {}", file.getAbsolutePath()); return null; } } /** * Tests if file is UTF-8 encoded with or without BOM. * * @param file File to test * @return True if file is UTF-8 encoded with or without BOM, false otherwise. * @throws IOException */ public static boolean isFileUTF8(File file) throws IOException { return isCharsetUTF8(getFileCharset(file)); } /** * Tests if charset is UTF-8. * * @param charset <code>Charset</code> to test * @return True if charset is UTF-8, false otherwise. */ public static boolean isCharsetUTF8(Charset charset) { return charset != null && charset.equals(StandardCharsets.UTF_8); } /** * Tests if charset is UTF-8. * * @param charset charset name to test * @return True if charset is UTF-8, false otherwise. */ public static boolean isCharsetUTF8(String charsetName) { return equalsIgnoreCase(charsetName, CHARSET_UTF_8); } /** * Tests if file is UTF-16 encoded. * * @param file File to test * @return True if file is UTF-16 encoded, false otherwise. * @throws IOException */ public static boolean isFileUTF16(File file) throws IOException { return isCharsetUTF16(getFileCharset(file)); } /** * Tests if charset is UTF-16. * * @param charset <code>Charset</code> to test * @return True if charset is UTF-16, false otherwise. */ public static boolean isCharsetUTF16(Charset charset) { return charset != null && (charset.equals(StandardCharsets.UTF_16) || charset.equals(StandardCharsets.UTF_16BE) || charset.equals(StandardCharsets.UTF_16LE)); } /** * Tests if charset is UTF-16. * * @param charset charset name to test * @return True if charset is UTF-16, false otherwise. */ public static boolean isCharsetUTF16(String charsetName) { return (equalsIgnoreCase(charsetName, CHARSET_UTF_16LE) || equalsIgnoreCase(charsetName, CHARSET_UTF_16BE)); } /** * Tests if charset is UTF-32. * * @param charsetName charset name to test * @return True if charset is UTF-32, false otherwise. */ public static boolean isCharsetUTF32(String charsetName) { return (equalsIgnoreCase(charsetName, CHARSET_UTF_32LE) || equalsIgnoreCase(charsetName, CHARSET_UTF_32BE)); } /** * Converts UTF-16 inputFile to UTF-8 outputFile. Does not overwrite existing outputFile file. * * @param inputFile UTF-16 file * @param outputFile UTF-8 file after conversion * @throws IOException */ public static void convertFileFromUtf16ToUtf8(File inputFile, File outputFile) throws IOException { Charset charset; if (inputFile == null || !inputFile.canRead()) { throw new FileNotFoundException("Can't read inputFile."); } try { charset = getFileCharset(inputFile); } catch (IOException ex) { LOGGER.debug("Exception during charset detection.", ex); throw new IllegalArgumentException("Can't confirm inputFile is UTF-16."); } if (isCharsetUTF16(charset)) { if (!outputFile.exists()) { BufferedReader reader = null; /* * This is a strange hack, and I'm not sure if it's needed. I * did it this way to conform to the tests, which dictates that * UTF-16LE should produce UTF-8 without BOM while UTF-16BE * should produce UTF-8 with BOM. * * For some reason creating a FileInputStream with UTF_16 produces * an UTF-8 outputfile without BOM, while using UTF_16LE or * UTF_16BE produces an UTF-8 outputfile with BOM. * @author Nadahar */ if (charset.equals(StandardCharsets.UTF_16LE)) { reader = new BufferedReader(new InputStreamReader(new FileInputStream(inputFile), StandardCharsets.UTF_16)); } else { reader = new BufferedReader(new InputStreamReader(new FileInputStream(inputFile), charset)); } BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile), StandardCharsets.UTF_8)); int c; while ((c = reader.read()) != -1) { writer.write(c); } writer.close(); reader.close(); } } else { throw new IllegalArgumentException("File is not UTF-16"); } } /** * Return a file or folder's permissions.<br><br> * * This should <b>NOT</b> be used for checking e.g. read permissions before * trying to open a file, because you can't assume that the same is true * when you actually open the file. Other threads or processes could have * locked the file (or changed it's permissions) in the meanwhile. Instead, * use e.g <code>FileNotFoundException</code> like this: * <pre><code> * } catch (FileNotFoundException e) { * LOGGER.debug("Can't read xxx {}", e.getMessage()); * } * </code></pre> * <code>e.getMessage()</code> will contain both the full path to the file * the reason it couldn't be read (e.g. no permission). * * @param file The file or folder to check permissions for * @return A <code>FilePermissions</code> object holding the permissions * @throws FileNotFoundException * @see {@link #getFilePermissions(String)} */ public static FilePermissions getFilePermissions(File file) throws FileNotFoundException { return new FilePermissions(file); } /** * Like {@link #getFilePermissions(File)} but returns <code>null</code> * instead of throwing <code>FileNotFoundException</code> if the file or * folder isn't found. */ public static FilePermissions getFilePermissionsNoThrow(File file) { try { return new FilePermissions(file); } catch (FileNotFoundException | IllegalArgumentException e) { return null; } } /** * Return a file or folder's permissions.<br><br> * * This should <b>NOT</b> be used for checking e.g. read permissions before * trying to open a file, because you can't assume that the same is true * when you actually open the file. Other threads or processes could have * locked the file (or changed it's permissions) in the meanwhile. Instead, * use e.g <code>FileNotFoundException</code> like this: * <pre><code> * } catch (FileNotFoundException e) { * LOGGER.debug("Can't read xxx {}", e.getMessage()); * } * </code></pre> * <code>e.getMessage()</code> will contain both the full path to the file * the reason it couldn't be read (e.g. no permission). * * @param path The file or folder name to check permissions for * @return A <code>FilePermissions</code> object holding the permissions * @throws FileNotFoundException * @see {@link #getFilePermissions(File)} */ public static FilePermissions getFilePermissions(String path) throws FileNotFoundException { if (path != null) { return new FilePermissions(new File(path)); } else { File file = null; return new FilePermissions(file); } } /** * Like {@link #getFilePermissions(String)} but returns <code>null</code> * instead of throwing <code>FileNotFoundException</code> if the file or * folder isn't found. */ public static FilePermissions getFilePermissionsNoThrow(String path) { if (path != null) { try { return new FilePermissions(new File(path)); } catch (FileNotFoundException | IllegalArgumentException e) { return null; } } else { return null; } } public static boolean isFileRelevant(File f, PmsConfiguration configuration) { String fileName = f.getName().toLowerCase(); if ( ( configuration.isArchiveBrowsing() && ( fileName.endsWith(".zip") || fileName.endsWith(".cbz") || fileName.endsWith(".rar") || fileName.endsWith(".cbr") ) ) || fileName.endsWith(".iso") || fileName.endsWith(".img") || fileName.endsWith(".m3u") || fileName.endsWith(".m3u8") || fileName.endsWith(".pls") || fileName.endsWith(".cue") ) { return true; } return false; } public static boolean isFolderRelevant(File f, PmsConfiguration configuration) { return isFolderRelevant(f, configuration, Collections.<String>emptySet()); } public static boolean isFolderRelevant(File f, PmsConfiguration configuration, Set<String> ignoreFiles) { if (f.isDirectory() && configuration.isHideEmptyFolders()) { File[] children = f.listFiles(); /** * listFiles() returns null if "this abstract pathname does not denote a directory, or if an I/O error occurs". * in this case (since we've already confirmed that it's a directory), this seems to mean the directory is non-readable * http://www.ps3mediaserver.org/forum/viewtopic.php?f=6&t=15135 * http://stackoverflow.com/questions/3228147/retrieving-the-underlying-error-when-file-listfiles-return-null */ if (children == null) { LOGGER.warn("Can't list files in non-readable directory: {}", f.getAbsolutePath()); } else { for (File child : children) { if (ignoreFiles.contains(child.getAbsolutePath())) { continue; } if (child.isFile()) { if (FormatFactory.getAssociatedFormat(child.getName()) != null || isFileRelevant(child, configuration)) { return true; } } else { if (isFolderRelevant(child, configuration, ignoreFiles)) { return true; } } } } } return false; } public static String renameForSorting(String filename) { if (PMS.getConfiguration().isPrettifyFilenames()) { // This makes anime sort properly filename = removeGroupNameFromBeginning(filename); // Replace periods and underscores with spaces filename = filename.replaceAll("\\.|_", " "); } if (PMS.getConfiguration().isIgnoreTheWordAandThe()) { // Remove "a" and "the" from filename filename = filename.replaceAll("^(?i)A[ .]|The[ .]", ""); // Replace multiple whitespaces with space filename = filename.replaceAll("\\s{2,}"," "); } return filename; } public static BufferedReader bufferedReaderWithCorrectCharset(File file) throws IOException { BufferedReader reader; Charset fileCharset = getFileCharset(file); if (fileCharset != null) { reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), fileCharset)); } else { reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)); LOGGER.warn("Could not detect character encoding for file \"{}\". It will probably be interpreted wrong", file.getAbsolutePath()); } return reader; } /** * Checks for valid file name syntax. Path is not allowed. * * @param fileName the file name to be verified * @return whether or not the file name is valid */ public static boolean isValidFileName(String fileName) { if (Platform.isWindows()) { if (fileName.matches("^[^\"*:<>?/\\\\]+$")) { return true; } } else if (Platform.isMac()) { if (fileName.matches("^[^:/]+$")) { return true; } } else { // Assuming POSIX if (fileName.matches("^[A-Za-z0-9._][A-Za-z0-9._-]*$")) { return true; } } return false; } /** * Appends a path separator of the same type last in the string if * it's not already there. * @param path the path to be modified * @return the corrected path */ public static String appendPathSeparator(String path) { if (!path.endsWith("\\") && !path.endsWith("/")) { if (path.contains("\\")) { path += "\\"; } else { path += "/"; } } return path; } private static Boolean isAdmin = null; private static Object isAdminLock = new Object(); /** * Determines whether or not the program has admin/root permissions. */ public static boolean isAdmin() { synchronized(isAdminLock) { if (isAdmin != null) { return isAdmin; } if (Platform.isWindows()) { Float ver = null; try { ver = Float.valueOf(System.getProperty("os.version")); } catch (NullPointerException | NumberFormatException e) { LOGGER.error( "Could not determine Windows version from {}. Administrator privileges is undetermined: {}", System.getProperty("os.version"), e.getMessage() ); isAdmin = false; return false; } if (ver >= 5.1) { try { String command = "reg query \"HKU\\S-1-5-19\""; Process p = Runtime.getRuntime().exec(command); p.waitFor(); int exitValue = p.exitValue(); if (0 == exitValue) { isAdmin = true; return true; } isAdmin = false; return false; } catch (IOException | InterruptedException e) { LOGGER.error("An error prevented UMS from checking Windows permissions: {}", e.getMessage()); } } else { isAdmin = true; return true; } } else if (Platform.isLinux() || Platform.isMac()) { try { final String command = "id -Gn"; LOGGER.trace("isAdmin: Executing \"{}\"", command); Process p = Runtime.getRuntime().exec(command); InputStream is = p.getInputStream(); InputStreamReader isr = new InputStreamReader(is, StandardCharsets.US_ASCII); int exitValue; String exitLine; try (BufferedReader br = new BufferedReader(isr)) { p.waitFor(); exitValue = p.exitValue(); exitLine = br.readLine(); } if (exitValue != 0 || exitLine == null || exitLine.isEmpty()) { LOGGER.error("Could not determine root privileges, \"{}\" ended with exit code: {}", command, exitValue); isAdmin = false; return false; } LOGGER.trace("isAdmin: \"{}\" returned {}", command, exitLine); if ((Platform.isLinux() && exitLine.matches(".*\\broot\\b.*")) || (Platform.isMac() && exitLine.matches(".*\\badmin\\b.*"))) { LOGGER.trace("isAdmin: UMS has {} privileges", Platform.isLinux() ? "root" : "admin"); isAdmin = true; return true; } LOGGER.trace("isAdmin: UMS does not have {} privileges", Platform.isLinux() ? "root" : "admin"); isAdmin = false; return false; } catch (IOException | InterruptedException e) { LOGGER.error("An error prevented UMS from checking {} permissions: {}", Platform.isMac() ? "OS X" : "Linux" ,e.getMessage()); } } isAdmin = false; return false; } } /** * Finds the {@link UnixMountPoint} for a {@link java.nio.file.Path} given * that the file resides on a Unix file system. * * @param path the {@link java.nio.file.Path} for which to find the Unix mount point. * @return The {@link UnixMountPoint} for the given path. * * @throws InvalidFileSystemException */ public static UnixMountPoint getMountPoint(Path path) throws InvalidFileSystemException { UnixMountPoint mountPoint = new UnixMountPoint(); FileStore store; try { store = Files.getFileStore(path); } catch (IOException e) { throw new InvalidFileSystemException( String.format("Could not get Unix mount point for file \"%s\": %s", path.toAbsolutePath(), e.getMessage()), e ); } try { Field entryField = store.getClass().getSuperclass().getDeclaredField("entry"); Field nameField = entryField.getType().getDeclaredField("name"); Field dirField = entryField.getType().getDeclaredField("dir"); entryField.setAccessible(true); nameField.setAccessible(true); dirField.setAccessible(true); mountPoint.device = new String((byte[]) nameField.get(entryField.get(store)), StandardCharsets.UTF_8); mountPoint.folder = new String((byte[]) dirField.get(entryField.get(store)), StandardCharsets.UTF_8); return mountPoint; } catch (NoSuchFieldException e) { throw new InvalidFileSystemException(String.format("File \"%s\" is not on a Unix file system", path.isAbsolute()), e); } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) { throw new InvalidFileSystemException( String.format("An error occurred while trying to find mount point for file \"%s\": %s", path.toAbsolutePath(), e.getMessage()), e ); } } /** * Finds the {@link UnixMountPoint} for a {@link java.io.File} given * that the file resides on a Unix file system. * * @param file the {@link java.io.File} for which to find the Unix mount point. * @return The {@link UnixMountPoint} for the given path. * * @throws InvalidFileSystemException */ public static UnixMountPoint getMountPoint(File file) throws InvalidFileSystemException { return getMountPoint(file.toPath()); } public static boolean isUnixStickyBit(Path path) throws IOException, InvalidFileSystemException { PosixFileAttributes attr = Files.readAttributes(path, PosixFileAttributes.class); try { Field st_modeField = attr.getClass().getDeclaredField("st_mode"); st_modeField.setAccessible(true); int st_mode = st_modeField.getInt(attr); return (st_mode & S_ISVTX) > 0; } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { throw new InvalidFileSystemException("File is not on a Unix file system: " + e.getMessage(), e); } } private static int unixUID = Integer.MIN_VALUE; private static Object unixUIDLock = new Object(); /** * Gets the user ID on Unix based systems. This should not change during a * session and the lookup is expensive, so we cache the result. * * @return The Unix user ID * @throws IOException */ public static int getUnixUID() throws IOException { if ( Platform.isAIX() || Platform.isFreeBSD() || Platform.isGNU() || Platform.iskFreeBSD() || Platform.isLinux() || Platform.isMac() || Platform.isNetBSD() || Platform.isOpenBSD() || Platform.isSolaris() ) { synchronized (unixUIDLock) { if (unixUID < 0) { String response; Process id; id = Runtime.getRuntime().exec("id -u"); try (BufferedReader reader = new BufferedReader(new InputStreamReader(id.getInputStream(), Charset.defaultCharset()))) { response = reader.readLine(); } try { unixUID = Integer.parseInt(response); } catch (NumberFormatException e) { throw new UnsupportedOperationException("Unexpected response from OS: " + response, e); } } return unixUID; } } else { throw new UnsupportedOperationException("getUnixUID can only be called on Unix based OS'es"); } } }