% FilenameUtils - a collection of utilities for dealing with Filenames on
% clusters.

% Copyright 2011-2020 The MathWorks, Inc.

classdef ( Hidden, Sealed ) FilenameUtils
    methods ( Static )

        function clientRoot = getClientRootFromStruct( dlStruct )
        % Given a DataLocation struct from storage with pc/unix fields, return
        % the location appropriate for the client.
            validateattributes( dlStruct, {'struct'}, {'scalar'} );

            if ispc
                clientRoot = dlStruct.windows;
            else
                clientRoot = dlStruct.unix;
            end
        end

        function [clientName, clusterName] = jobSpecificFilename( clusterOsTypeStr, dlStruct, jobSubDir, ...
                                                              nameInsideDir, isOkIfNoClusterStorage )
        % jobSpecificFilename() - return a filename inside the storage
        % - clusterOsTypeStr: 'pc', 'windows', 'unix', 'mixed'
        % - dlStruct: struct with fields 'windows' and 'unix' for data location root
        % - jobSubDir: the name of the directory containing the job
        % - nameInsideDir: the name of the file within the directory
        % - isOkIfNoClusterStorage: true if call site does not require clusterName
        % returning:
        % - clientName: a filename valid on the client
        % - clusterName: a filename valid on the cluster

            import parallel.internal.apishared.FilenameUtils;
            import parallel.internal.apishared.OsType;

            clusterOsType = OsType.fromName( clusterOsTypeStr ); % validates
            validateattributes( dlStruct, { 'struct' }, {} );
            validateattributes( jobSubDir, {'char'}, {} );
            validateattributes( nameInsideDir, {'char'}, {} );
            validateattributes( isOkIfNoClusterStorage, {'logical'}, {'scalar'} );

            clientDir = fullfile( FilenameUtils.getClientRootFromStruct( dlStruct ), ...
                                  jobSubDir );

            winDir  = dlStruct.windows;
            unixDir = dlStruct.unix;

            if clusterOsType == OsType.PC
                gotClusterStorage = ~isempty( winDir );
                clusterDir = fullfile( winDir, jobSubDir );
            elseif clusterOsType == OsType.UNIX
                gotClusterStorage = ~isempty( unixDir );
                clusterDir = fullfile( unixDir, jobSubDir );
            else
                % We've got a mixed cluster. Things are only OK if it's OK not to have
                % cluster storage.
                if ~isOkIfNoClusterStorage
                    % Never get here - parallel submission doesn't allow mixed, and distributed
                    % submission is OK with no cluster storage.
                    error(message('parallel:cluster:CannotCalculateClusterStorage'));
                end

                % Just use the storage for the client
                gotClusterStorage = true;
                clusterDir = clientDir;
            end

            if gotClusterStorage || isOkIfNoClusterStorage
                % We're fine then - we'll just the nameInsideDir part
            else
                error(message('parallel:cluster:NoClusterStorage', clusterOsTypeStr));
            end

            if gotClusterStorage
                if clusterOsType == OsType.MIXED
                    osTypeForSlash = OsType.forClient();
                else
                    osTypeForSlash = clusterOsType;
                end
                clusterName = FilenameUtils.fixSlashes( ...
                    fullfile( clusterDir, nameInsideDir ), osTypeForSlash );
            else
                clusterName = nameInsideDir;
            end
            clientName = FilenameUtils.fixSlashes( fullfile( clientDir, nameInsideDir ) );
        end

        function str = fixSlashes( inStr, osTypeMaybe )
        % fixSlashes( inStr, osType ) - fix / and \ in a string.
        % - inStr: a string to fix
        % - osTypeMaybe: {'pc', 'windows'}, 'unix'; or an OsType
        % if osTypeMaybe is not specified, use the current system type.

            import parallel.internal.apishared.OsType;

            narginchk( 1, 2 );
            validateattributes( inStr, {'char', 'cell'}, {} );
            if nargin < 2
                osType = OsType.forClient();
            else
                if isa( osTypeMaybe, 'parallel.internal.apishared.OsType' )
                    osType = osTypeMaybe;
                else
                    % Assume char, OsType.fromName validates
                    osType = OsType.fromName( osTypeMaybe );
                end
            end

            switch osType
              case OsType.PC
                goodSlash = '\';
                badSlash  = '/';
              case OsType.UNIX
                goodSlash = '/';
                badSlash  = '\';
              otherwise
                error(message('parallel:cluster:InvalidOperatingSystemType', osType.Api2Name));
            end
            str = strrep( inStr, badSlash, goodSlash );
        end
        
        function absFile = getAbsolutePath( filename )
            % The canonical path is always an absolute path
            absFile = parallel.internal.apishared.FilenameUtils.getCanonicalPath( filename );
        end

        function canonicalPath = getCanonicalPath( rawPath )
        % Obtains a canonical version of a path, which can be safely used
        % for path comparisons.
            if isempty( rawPath )
                rawPath = pwd(); % Treat an empty path as the current working directory
            end
            
            canonicalPath = parallel.internal.filesystem.getCanonicalPath( rawPath );
            canonicalPath = dctReplaceDriveWithUNCPath( canonicalPath );
        end

        function writable = isWritable( filename )
            [success, values] = fileattrib( filename );
            
             % If fileattrib() is unsuccessful, the writability of the file
             % cannot be guaranteed, so return false
            writable = success && values.UserWrite;
        end

        function qstr = quoteForClient( path )
        % quoteForClient - wrap a path in quotes appropriate for the client OS
        % prior to shell execution.
            if ispc
                quote = '"';
            else
                quote = '''';
            end
            if ~isempty( path ) && path(1) ~= quote
                qstr = [ quote, path, quote ];
            else
                qstr = path;
            end
        end

        function isRelative = isRelativePath( baseDir, absolutePath )
            % This checks if the absolutePath is within the baseDir
            % isRelative is true when absolutePath is contained with baseDir,
            % false otherwise. Assumes both inputs are absolute, neither
            % input has trailing slashes, and have platform-appropriate
            % file separators.
            
            % baseDir must be shorter than absolutePath, if there is to be
            % a relative path between them.
            if length(baseDir) >= length(absolutePath)
                isRelative = false;
            else                
                % The common part simply starts at the end of the base dir.
                baseDirStringsMatch = strncmp(absolutePath, baseDir, length(baseDir));
                
                % To handle following case, we check if the next character
                % after the baseDirStringMatch is a file separator.
                %
                %   isRelative should be false in the following case:
                %   baseDir      = 's:\wibble\wobble'
                %   absolutePath = 's:\wibble\wobblewabble'
                %                                   ^ check if this is a
                %                                   file separator.
                %
                % Note: length(baseDir) < length(absolutePath), 
                % so absolutePath(length(baseDir)+1) is safe.
                nextCharIsFileSeparator = strcmp(absolutePath(length(baseDir)+1), filesep);
                
                isRelative = baseDirStringsMatch && nextCharIsFileSeparator;
            end
        end
        
        function relative = getRelativePath( baseDir, absolutePath )
        % This gets the relative path from baseDir to the path specified in
        % absolutePath. It does not include any '..' directories, and will
        % return the absolute path if it cannot compute a relative path.
        % Assumes both inputs are absolute, neither input has trailing
        % slashes, and have platform-appropriate file separators.
            
            % TODO: g1443485 Support relative directories that require '..'
            if parallel.internal.apishared.FilenameUtils.isRelativePath( baseDir, absolutePath )
                relative = absolutePath(length(baseDir)+1:end);
                
                % Remove leading slashes
                if strcmp(relative(1), '/') || strcmp(relative(1), '\')
                    relative = relative(2:end);
                end
            else
                relative = absolutePath;
            end
        end

        function isSpecial = isSpecialDirectory( dirName )
            % isSpecialDirectory - return whether the given directory name represents
            %   a directory that has a isSpecial meaning to MATLAB.

            % This pattern matches names that begin with one of +, @ or private
            pattern = '^(\+.*|@.*|private)$';
            m = regexp( dirName, pattern, 'match' );
            isSpecial = ~isempty( m );
        end

        function [pathStart, pathEnd, foundSpecial] = specialSplitPath( pathIn )
        % specialSplitPath - Splits a path at the point it first encounters a +Package
        %    @Class, or private directory. Note that these directories are not
        %    special unless they exist as trailing directories on the path.
        %
        %    NOTE: The path input must be a directory not a file.

            pathStart = pathIn;
            pathEnd = '';
            foundSpecial = false;
            % Start with the last element of the path.
            [~, currentPathElement] = fileparts( pathIn );
            % Keep looping while the current path element is a special directory.
            while ~isempty( pathStart )
                if parallel.internal.apishared.FilenameUtils.isSpecialDirectory( currentPathElement )
                    % The parent directory is special, now we need to keep traversing
                    % the path until we are no longer within a special directory, and
                    % keep a record of the rest of the path.
                    pathEnd = fullfile( currentPathElement, pathEnd );
                    foundSpecial = true;
                else
                    break;
                end
                [currentPathElement, pathStart] = ...
                    parallel.internal.apishared.FilenameUtils.getParentDirectory( pathStart );
            end
        end

        function [parentDirName, parentDirPath] = getParentDirectory( pathIn )
        % getParentDirectory - Given a file or directory, return its parent directory.

            % Remove trailing slashes.
            pathIn = parallel.internal.apishared.FilenameUtils.removeTrailingSlash( pathIn );

            % We simply use fileparts twice, the first gives us the absolute path to
            % the parentDirectory, and the second gives us the name.
            parentDirPath = fileparts( pathIn );
            [~, parentDirName] = fileparts( parentDirPath );
        end

        function pathOut = removeTrailingSlash( pathIn )
        % removeTrailingSlash - Remove any trailing slashes from the path
            pathOut = regexprep( pathIn, '[\\/]+$', '' );
        end

        function pathOut = stripRoot( pathIn )
        % stripRoot - Remove the root from an absolute path.
        %
        %   Assumes all paths are absolute, this removes the root turning it into a path
        %   relative to the root. On windows it converts drive letters to a top level
        %   directory e.g. s:/1/2/3/file1.m -> s/1/2/3/file1.m
            if ispc
                % Match [driveLetter]:\rest\of\path or
                % [driveLetter]:/rest/of/path or [driveLetter]:\
                driveLetterPattern = '^(?<driveLetter>[a-zA-Z]):[\\/]+(?<relativePath>.*)';
                % or [driveLetter]:
                driveLetterOnlyPattern = '^(?<driveLetter>[a-zA-Z]):$';
                % or \\rest\of\path or //rest/of/path
                uncPattern = '^[\\/]{2,}(?<relativePath>.*)';
                % {driveLetterPattern, driveLetterOnlyPattern, uncPattern}, ...
                pattern = strjoin( ...
                    {driveLetterPattern, driveLetterOnlyPattern, uncPattern}, ...
                    '|' );
                matches = regexp( pathIn, pattern, 'names' );
                if ~isempty( matches )
                    if ~isempty( matches.driveLetter )
                        pathOut = [ matches.driveLetter '/' matches.relativePath ];
                    else
                        pathOut = matches.relativePath;
                    end
                else
                    pathOut = pathIn;
                end
            else
                pattern = '^/(?<relativePath>.*)';
                pathOut = regexprep( pathIn, pattern, '$<relativePath>' );
            end
        end

        function [allFiles, allDirectories] = listAll( rootDir, fileFilterFcn, dirFilterFcn )
        % listAll - List all the files and directories under 'rootDir' recursively.
        %
        %   listAll( rootDir, fileFilterFcn, dirFilterFcn ) - filters the files
        %   and directories found using the supplied filter functions, and does
        %   not recurse into those directories.  The filter function takes a
        %   string of the path and returns a boolean which if false removes it
        %   from the listing.
        %
        %   If 'rootDir' is not a directory, it will be returned in 'allFiles'
        
            if nargin < 2
                fileFilterFcn = @iDefaultFilter;
            end
            if nargin < 3
                dirFilterFcn = @iDefaultFilter;
            end

            % All files and directories will be listed with absolute paths, to
            % do this we need to start with the rootDir as an absolute path.
            rootDir = parallel.internal.apishared.FilenameUtils.getAbsolutePath( rootDir );

            currentDirectories = { rootDir };
            allFiles = {};
            allDirectories = {};

            % rootDir must be a directory, if it isn't, then rather than error,
            % simply return the input. We should only return it if the filter
            % function does not remove it.
            if ~isfolder( rootDir )
                if fileFilterFcn( rootDir )
                    allFiles = { rootDir };
                end
                return;
            end

            while ~isempty( currentDirectories )
                % Pop the top directory off the list of current directories
                thisDir = currentDirectories{1};
                currentDirectories = currentDirectories(2:end);
                
                % Add it to the list of all directories
                allDirectories = [allDirectories thisDir]; %#ok<AGROW>

                % Get all the files and directories in the current directory.
                dirContents = dir( thisDir );
                filteredFiles = iFilterFiles( dirContents, fileFilterFcn );
                filteredDirectories = iFilterDirectories( dirContents, dirFilterFcn );
            
                % Prepend the current directory we are in to the directories.
                filteredDirectories = cellfun( @(d) fullfile( thisDir, d, '' ), ...
                    filteredDirectories, 'UniformOutput', false );
                filteredFiles = cellfun( @(f) fullfile( thisDir, f ), filteredFiles, ...
                    'UniformOutput', false );
                allFiles = [allFiles filteredFiles]; %#ok<AGROW>
                currentDirectories = [currentDirectories filteredDirectories]; %#ok<AGROW>
            end
        end


        function out = isLeafDirectory( aDir )
            % A 'leaf' directory is one that contains files or is empty, i.e.
            % does not solely contain directories.
            files = dir( aDir );
            % Filter out '.' and '..' as these do not count as 'real' directories.
            dotDirs = cellfun( @iIsDotDirectoryOrEmpty, {files.name} );
            files = files( ~dotDirs );
            out = isempty( files ) || any( ~[files.isdir] );
        end

        function [resolved, success] = safeWhich( fullRelPartial )
        % safeWhich - call which - ignoring errors. 'which' can under certain
        % circumstances attempt to evaluate the entity you're trying to resolve, and can
        % potentially error - we don't want that. See G774988 for more. We also try to
        % deal with the fact that 'which' might find a builtin rather than a directory
        % of the same name, so we call 'exist' on the result.

            try
                fromWhich = which( fullRelPartial );
                if ~isempty( fromWhich )
                    success = true;
                    if exist( fromWhich, 'file' )
                        resolved = fromWhich;
                    else
                        resolved = fullRelPartial;
                    end
                else
                    resolved = fullRelPartial;
                    success = false;
                end
            catch E %#ok<NASGU>
                % just return the input
                resolved = fullRelPartial;
                success = false;
            end
        end

        function errorIfNotFileOrDirectory( pathIn )
            if ~exist( pathIn, 'file' ) && ~exist( pathIn, 'dir' )
                error( message( 'parallel:cluster:FileDoesNotExist', pathIn ) );
            end
        end
        
        function folderIntersection = intersectWithFolderTree( setOfFolders, folderTreeRoot, folderTreeRootCache )
            % intersectWithFolderTree - list all the subfolders of folderTreeRoot
            % whose absolute path is contained in the setOfFolders
            if nargin == 3
                subFoldersAll = folderTreeRootCache( 2:end );
            else                
                [~, folderTree] = parallel.internal.apishared.FilenameUtils.listAll( folderTreeRoot );           
                subFoldersAll = folderTree( 2:end );
            end
            folderIntersection = intersect( subFoldersAll, setOfFolders );
        end
        
        function result = isAbsolutePath( fileOrFolder )
            % isAbsolutePath - helper method to determine if the given path is located in absolute path
            result = parallel.internal.filesystem.isAbsolutePath( fileOrFolder );
        end

        function success = fileExists( file )
            % Check that the given file exists. Note that exist(..., 'file')
            % checks both files and directories, so we explicitly ensure
            % that the file is a file and not a dir.

            status = exist( file, 'file' );
            success = status > 0 && status ~= 7;
        end

        function tf = exists( fileOrFolder )
            % Check that the given file or folder exists
            tf = parallel.internal.filesystem.exists( fileOrFolder );
        end
        
        function sameFile = isSameFile(file1, file2)
            % Check whether file1 and file2 point to the same underlying
            % file.
            sameFile = parallel.internal.filesystem.equivalent(file1, file2);
        end
        
        % Returns a logical array the same size as fileOrFiles in which
        % each element of the array is true if and only if the file is
        % contained in parentDir
        function tf = containsFile(parentDir, fileOrFiles)
            import parallel.internal.apishared.FilenameUtils
            
            fileOrFiles = cellstr(fileOrFiles); % Convert all inputs to cellstr
            tf = false(size(fileOrFiles)); % Allocate default values
            
            % Quick exit if there are no files to check, the parent
            % directory doesn't exist, or the parent directory isn't
            % actually a folder
            if isempty(fileOrFiles) || ~FilenameUtils.exists(parentDir) || ~isfolder(parentDir)
                return
            end
            
            % Ensure that parentDir is properly canonicalized so that we
            % can perform string comparisons
            parentDir = FilenameUtils.getCanonicalPath(parentDir);
            
            for ii = 1:numel(fileOrFiles)
                % Use the canonical path of the file to ensure that we can
                % perform string comparisons
                file = FilenameUtils.getCanonicalPath(fileOrFiles{ii});
                
                % NB: The file must exist for it to be contained within a
                % directory!
                tf(ii) = FilenameUtils.exists(file) && startsWith(file, parentDir);
            end
        end
    end
end

%--------------------------------------------------------------------------
function out = iIsDotDirectoryOrEmpty( aDir )
% aDir may be relative or absolute, we're only interested in the final
% directory name.
[~, dirName] = fileparts( aDir );

if ~isempty( dirName )
    out = strcmpi( dirName, '.' ) || strcmpi( dirName, '..' );
else
    out = true;
end
end

%--------------------------------------------------------------------------
function out = iDefaultFilter( ~ )
% The default filter does not filter anything.
out = true;
end

function filteredDirectories = iFilterDirectories( dirContents, filterFcn )
% iFilterDirectories - Filter the results of dir() to give only directory names.
%
%   iFilterDirectories( dirContents, filterFcn ) - filterFcn is a function
%   handle which takes a string and returns true if it should be kept in
%   the listing or false if it should be removed.
    if nargin < 2
        filterFcn = @iDefaultFilter;
    end

    % Filter the listing to leave only the directories.
    directories = dirContents([dirContents.isdir]);
    directories = {directories.name};

    % 'dir' also lists '.' and '..', we need to remove these to
    % prevent an infinite loop.
    dotDirs = cellfun( @iIsDotDirectoryOrEmpty, directories );
    filterIdx = cellfun( filterFcn, directories );
    filteredDirectories = directories( filterIdx & ~dotDirs );
end

function filteredFiles = iFilterFiles( dirContents, filterFcn )
% iFilterFiles - Filter the results of dir() to give only files.
%
%   iFilterFiles( dirContents, filterFcn ) - filterFcn is a function
%   handle which takes a string and returns true if it should be kept
%   in the listing or false if it should be removed.
    if nargin < 2
        filterFcn = @iDefaultFilter;
    end
    % Filter the listing to leave only the files.
    files = dirContents(~[dirContents.isdir]);
    files = {files.name};

    filterIdx = cellfun( filterFcn, files );
    filteredFiles = files( filterIdx );
end
