"Fossies" - the Fresh Open Source Software Archive

Member "asciidoctor-2.0.10/lib/asciidoctor/path_resolver.rb" (1 Jun 2019, 19447 Bytes) of package /linux/www/asciidoctor-2.0.10.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Ruby source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "path_resolver.rb": 2.0.7_vs_2.0.8.

    1 # frozen_string_literal: true
    2 module Asciidoctor
    3 # Public: Handles all operations for resolving, cleaning and joining paths.
    4 # This class includes operations for handling both web paths (request URIs) and
    5 # system paths.
    6 #
    7 # The main emphasis of the class is on creating clean and secure paths. Clean
    8 # paths are void of duplicate parent and current directory references in the
    9 # path name. Secure paths are paths which are restricted from accessing
   10 # directories outside of a jail path, if specified.
   11 #
   12 # Since joining two paths can result in an insecure path, this class also
   13 # handles the task of joining a parent (start) and child (target) path.
   14 #
   15 # This class makes no use of path utilities from the Ruby libraries. Instead,
   16 # it handles all aspects of path manipulation. The main benefit of
   17 # internalizing these operations is that the class is able to handle both posix
   18 # and windows paths independent of the operating system on which it runs. This
   19 # makes the class both deterministic and easier to test.
   20 #
   21 # Examples
   22 #
   23 #     resolver = PathResolver.new
   24 #
   25 #     # Web Paths
   26 #
   27 #     resolver.web_path('images')
   28 #     => 'images'
   29 #
   30 #     resolver.web_path('./images')
   31 #     => './images'
   32 #
   33 #     resolver.web_path('/images')
   34 #     => '/images'
   35 #
   36 #     resolver.web_path('./images/../assets/images')
   37 #     => './assets/images'
   38 #
   39 #     resolver.web_path('/../images')
   40 #     => '/images'
   41 #
   42 #     resolver.web_path('images', 'assets')
   43 #     => 'assets/images'
   44 #
   45 #     resolver.web_path('tiger.png', '../assets/images')
   46 #     => '../assets/images/tiger.png'
   47 #
   48 #     # System Paths
   49 #
   50 #     resolver.working_dir
   51 #     => '/path/to/docs'
   52 #
   53 #     resolver.system_path('images')
   54 #     => '/path/to/docs/images'
   55 #
   56 #     resolver.system_path('../images')
   57 #     => '/path/to/images'
   58 #
   59 #     resolver.system_path('/etc/images')
   60 #     => '/etc/images'
   61 #
   62 #     resolver.system_path('images', '/etc')
   63 #     => '/etc/images'
   64 #
   65 #     resolver.system_path('', '/etc/images')
   66 #     => '/etc/images'
   67 #
   68 #     resolver.system_path(nil, nil, '/path/to/docs')
   69 #     => '/path/to/docs'
   70 #
   71 #     resolver.system_path('..', nil, '/path/to/docs')
   72 #     => '/path/to/docs'
   73 #
   74 #     resolver.system_path('../../../css', nil, '/path/to/docs')
   75 #     => '/path/to/docs/css'
   76 #
   77 #     resolver.system_path('../../../css', '../../..', '/path/to/docs')
   78 #     => '/path/to/docs/css'
   79 #
   80 #     resolver.system_path('..', 'C:\\data\\docs\\assets', 'C:\\data\\docs')
   81 #     => 'C:/data/docs'
   82 #
   83 #     resolver.system_path('..\\..\\css', 'C:\\data\\docs\\assets', 'C:\\data\\docs')
   84 #     => 'C:/data/docs/css'
   85 #
   86 #     begin
   87 #       resolver.system_path('../../../css', '../../..', '/path/to/docs', recover: false)
   88 #     rescue SecurityError => e
   89 #       puts e.message
   90 #     end
   91 #     => 'path ../../../../../../css refers to location outside jail: /path/to/docs (disallowed in safe mode)'
   92 #
   93 #     resolver.system_path('/path/to/docs/images', nil, '/path/to/docs')
   94 #     => '/path/to/docs/images'
   95 #
   96 #     begin
   97 #       resolver.system_path('images', '/etc', '/path/to/docs', recover: false)
   98 #     rescue SecurityError => e
   99 #       puts e.message
  100 #     end
  101 #     => start path /etc is outside of jail: /path/to/docs'
  102 #
  103 class PathResolver
  104   include Logging
  105 
  106   DOT = '.'
  107   DOT_DOT = '..'
  108   DOT_SLASH = './'
  109   SLASH = '/'
  110   BACKSLASH = '\\'
  111   DOUBLE_SLASH = '//'
  112   WindowsRootRx = /^(?:[a-zA-Z]:)?[\\\/]/
  113 
  114   attr_accessor :file_separator
  115   attr_accessor :working_dir
  116 
  117   # Public: Construct a new instance of PathResolver, optionally specifying the
  118   # file separator (to override the system default) and the working directory
  119   # (to override the present working directory). The working directory will be
  120   # expanded to an absolute path inside the constructor.
  121   #
  122   # file_separator - the String file separator to use for path operations
  123   #                  (optional, default: File::ALT_SEPARATOR or File::SEPARATOR)
  124   # working_dir    - the String working directory (optional, default: Dir.pwd)
  125   #
  126   def initialize file_separator = nil, working_dir = nil
  127     @file_separator = file_separator || ::File::ALT_SEPARATOR || ::File::SEPARATOR
  128     @working_dir = working_dir ? ((root? working_dir) ? (posixify working_dir) : (::File.expand_path working_dir)) : ::Dir.pwd
  129     @_partition_path_sys = {}
  130     @_partition_path_web = {}
  131   end
  132 
  133   # Public: Check whether the specified path is an absolute path.
  134   #
  135   # This operation considers both posix paths and Windows paths. The path does
  136   # not have to be posixified beforehand. This operation does not handle URIs.
  137   #
  138   # Unix absolute paths start with a slash. UNC paths can start with a slash or
  139   # backslash. Windows roots can start with a drive letter.
  140   #
  141   # path - the String path to check
  142   #
  143   # returns a Boolean indicating whether the path is an absolute root path
  144   def absolute_path? path
  145     (path.start_with? SLASH) || (@file_separator == BACKSLASH && (WindowsRootRx.match? path))
  146   end
  147 
  148   # Public: Check if the specified path is an absolute root path (or, in the
  149   # browser environment, an absolute URI as well)
  150   #
  151   # This operation considers both posix paths and Windows paths. If the JavaScript IO
  152   # module is xmlhttprequest, this operation also considers absolute URIs.
  153   #
  154   # Unix absolute paths and UNC paths start with slash. Windows roots can
  155   # start with a drive letter. When the IO module is xmlhttprequest (Opal
  156   # runtime only), an absolute (qualified) URI (starts with file://, http://,
  157   # or https://) is also considered to be an absolute path.
  158   #
  159   # path - the String path to check
  160   #
  161   # returns a Boolean indicating whether the path is an absolute root path (or
  162   # an absolute URI when the JavaScript IO module is xmlhttprequest)
  163   if RUBY_ENGINE == 'opal' && ::JAVASCRIPT_IO_MODULE == 'xmlhttprequest'
  164     def root? path
  165       (absolute_path? path) || (path.start_with? 'file://', 'http://', 'https://')
  166     end
  167   else
  168     alias root? absolute_path?
  169   end
  170 
  171   # Public: Determine if the path is a UNC (root) path
  172   #
  173   # path - the String path to check
  174   #
  175   # returns a Boolean indicating whether the path is a UNC path
  176   def unc? path
  177     path.start_with? DOUBLE_SLASH
  178   end
  179 
  180   # Public: Determine if the path is an absolute (root) web path
  181   #
  182   # path - the String path to check
  183   #
  184   # returns a Boolean indicating whether the path is an absolute (root) web path
  185   def web_root? path
  186     path.start_with? SLASH
  187   end
  188 
  189   # Public: Determine whether path descends from base.
  190   #
  191   # If path equals base, or base is a parent of path, return true.
  192   #
  193   # path - The String path to check. Can be relative.
  194   # base - The String base path to check against. Can be relative.
  195   #
  196   # returns If path descends from base, return the offset, otherwise false.
  197   def descends_from? path, base
  198     if base == path
  199       0
  200     elsif base == SLASH
  201       (path.start_with? SLASH) && 1
  202     else
  203       (path.start_with? base + SLASH) && (base.length + 1)
  204     end
  205   end
  206 
  207   # Public: Calculate the relative path to this absolute path from the specified base directory
  208   #
  209   # If neither path or base are absolute paths, the path is not contained
  210   # within the base directory, or the relative path cannot be computed, the
  211   # original path is returned work is done.
  212   #
  213   # path - [String] an absolute filename.
  214   # base - [String] an absolute base directory.
  215   #
  216   # Return the [String] relative path of the specified path calculated from the base directory.
  217   def relative_path path, base
  218     if root? path
  219       if (offset = descends_from? path, base)
  220         path.slice offset, path.length
  221       else
  222         begin
  223           (Pathname.new path).relative_path_from(Pathname.new base).to_s
  224         rescue
  225           path
  226         end
  227       end
  228     else
  229       path
  230     end
  231   end
  232 
  233   # Public: Normalize path by converting any backslashes to forward slashes
  234   #
  235   # path - the String path to normalize
  236   #
  237   # returns a String path with any backslashes replaced with forward slashes
  238   def posixify path
  239     if path
  240       @file_separator == BACKSLASH && (path.include? BACKSLASH) ? (path.tr BACKSLASH, SLASH) : path
  241     else
  242       ''
  243     end
  244   end
  245   alias posixfy posixify
  246 
  247   # Public: Expand the specified path by converting the path to a posix path, resolving parent
  248   # references (..), and removing self references (.).
  249   #
  250   # path - the String path to expand
  251   #
  252   # returns a String path as a posix path with parent references resolved and self references removed.
  253   # The result will be relative if the path is relative and absolute if the path is absolute.
  254   def expand_path path
  255     path_segments, path_root = partition_path path
  256     if path.include? DOT_DOT
  257       resolved_segments = []
  258       path_segments.each do |segment|
  259         segment == DOT_DOT ? resolved_segments.pop : resolved_segments << segment
  260       end
  261       join_path resolved_segments, path_root
  262     else
  263       join_path path_segments, path_root
  264     end
  265   end
  266 
  267   # Public: Partition the path into path segments and remove self references (.) and the trailing
  268   # slash, if present. Prior to being partitioned, the path is converted to a posix path.
  269   #
  270   # Parent references are not resolved by this method since the consumer often needs to handle this
  271   # resolution in a certain context (checking for the breach of a jail, for instance).
  272   #
  273   # path - the String path to partition
  274   # web  - a Boolean indicating whether the path should be handled
  275   #        as a web path (optional, default: false)
  276   #
  277   # Returns a 2-item Array containing the Array of String path segments and the
  278   # path root (e.g., '/', './', 'c:/', or '//'), which is nil unless the path is absolute.
  279   def partition_path path, web = nil
  280     if (result = (cache = web ? @_partition_path_web : @_partition_path_sys)[path])
  281       return result
  282     end
  283 
  284     posix_path = posixify path
  285 
  286     if web
  287       # ex. /sample/path
  288       if web_root? posix_path
  289         root = SLASH
  290       # ex. ./sample/path
  291       elsif posix_path.start_with? DOT_SLASH
  292         root = DOT_SLASH
  293       # else ex. sample/path
  294       end
  295     elsif root? posix_path
  296       # ex. //sample/path
  297       if unc? posix_path
  298         root = DOUBLE_SLASH
  299       # ex. /sample/path
  300       elsif posix_path.start_with? SLASH
  301         root = SLASH
  302       # ex. C:/sample/path (or file:///sample/path in browser environment)
  303       else
  304         root = posix_path.slice 0, (posix_path.index SLASH) + 1
  305       end
  306     # ex. ./sample/path
  307     elsif posix_path.start_with? DOT_SLASH
  308       root = DOT_SLASH
  309     # else ex. sample/path
  310     end
  311 
  312     path_segments = (root ? (posix_path.slice root.length, posix_path.length) : posix_path).split SLASH
  313     # strip out all dot entries
  314     path_segments.delete DOT
  315     cache[path] = [path_segments, root]
  316   end
  317 
  318   # Public: Join the segments using the posix file separator (since Ruby knows
  319   # how to work with paths specified this way, regardless of OS). Use the root,
  320   # if specified, to construct an absolute path. Otherwise join the segments as
  321   # a relative path.
  322   #
  323   # segments - a String Array of path segments
  324   # root     - a String path root (optional, default: nil)
  325   #
  326   # returns a String path formed by joining the segments using the posix file
  327   # separator and prepending the root, if specified
  328   def join_path segments, root = nil
  329     root ? %(#{root}#{segments.join SLASH}) : (segments.join SLASH)
  330   end
  331 
  332   # Public: Securely resolve a system path
  333   #
  334   # Resolve a system path from the target relative to the start path, jail path, or working
  335   # directory (specified in the constructor), in that order. If a jail path is specified, enforce
  336   # that the resolved path descends from the jail path. If a jail path is not provided, the resolved
  337   # path may be any location on the system. If the resolved path is absolute, use it as is (unless
  338   # it breaches the jail path). Expand all parent and self references in the resolved path.
  339   #
  340   # target - the String target path
  341   # start  - the String start path from which to resolve a relative target; falls back to jail, if
  342   #          specified, or the working directory specified in the constructor (default: nil)
  343   # jail   - the String jail path to which to confine the resolved path, if specified; must be an
  344   #          absolute path (default: nil)
  345   # opts   - an optional Hash of options to control processing (default: {}):
  346   #          * :recover is used to control whether the processor should
  347   #            automatically recover when an illegal path is encountered
  348   #          * :target_name is used in messages to refer to the path being resolved
  349   #
  350   # returns a String path relative to the start path, if specified, and confined to the jail path,
  351   # if specified. The path is posixified and all parent and self references in the path are expanded.
  352   def system_path target, start = nil, jail = nil, opts = {}
  353     if jail
  354       raise ::SecurityError, %(Jail is not an absolute path: #{jail}) unless root? jail
  355       #raise ::SecurityError, %(Jail is not a canonical path: #{jail}) if jail.include? DOT_DOT
  356       jail = posixify jail
  357     end
  358 
  359     if target
  360       if root? target
  361         target_path = expand_path target
  362         if jail && !(descends_from? target_path, jail)
  363           if opts.fetch :recover, true
  364             logger.warn %(#{opts[:target_name] || 'path'} is outside of jail; recovering automatically)
  365             target_segments, _ = partition_path target_path
  366             jail_segments, jail_root = partition_path jail
  367             return join_path jail_segments + target_segments, jail_root
  368           else
  369             raise ::SecurityError, %(#{opts[:target_name] || 'path'} #{target} is outside of jail: #{jail} (disallowed in safe mode))
  370           end
  371         end
  372         return target_path
  373       else
  374         target_segments, _ = partition_path target
  375       end
  376     else
  377       target_segments = []
  378     end
  379 
  380     if target_segments.empty?
  381       if start.nil_or_empty?
  382         return jail || @working_dir
  383       elsif root? start
  384         if jail
  385           start = posixify start
  386         else
  387           return expand_path start
  388         end
  389       else
  390         target_segments, _ = partition_path start
  391         start = jail || @working_dir
  392       end
  393     elsif start.nil_or_empty?
  394       start = jail || @working_dir
  395     elsif root? start
  396       start = posixify start if jail
  397     else
  398       #start = system_path start, jail, jail, opts
  399       start = %(#{(jail || @working_dir).chomp '/'}/#{start})
  400     end
  401 
  402     # both jail and start have been posixified at this point if jail is set
  403     if jail && (recheck = !(descends_from? start, jail)) && @file_separator == BACKSLASH
  404       start_segments, start_root = partition_path start
  405       jail_segments, jail_root = partition_path jail
  406       if start_root != jail_root
  407         if opts.fetch :recover, true
  408           logger.warn %(start path for #{opts[:target_name] || 'path'} is outside of jail root; recovering automatically)
  409           start_segments = jail_segments
  410           recheck = false
  411         else
  412           raise ::SecurityError, %(start path for #{opts[:target_name] || 'path'} #{start} refers to location outside jail root: #{jail} (disallowed in safe mode))
  413         end
  414       end
  415     else
  416       start_segments, jail_root = partition_path start
  417     end
  418 
  419     if (resolved_segments = start_segments + target_segments).include? DOT_DOT
  420       unresolved_segments, resolved_segments = resolved_segments, []
  421       if jail
  422         jail_segments, _ = partition_path jail unless jail_segments
  423         warned = false
  424         unresolved_segments.each do |segment|
  425           if segment == DOT_DOT
  426             if resolved_segments.size > jail_segments.size
  427               resolved_segments.pop
  428             elsif opts.fetch :recover, true
  429               unless warned
  430                 logger.warn %(#{opts[:target_name] || 'path'} has illegal reference to ancestor of jail; recovering automatically)
  431                 warned = true
  432               end
  433             else
  434               raise ::SecurityError, %(#{opts[:target_name] || 'path'} #{target} refers to location outside jail: #{jail} (disallowed in safe mode))
  435             end
  436           else
  437             resolved_segments << segment
  438           end
  439         end
  440       else
  441         unresolved_segments.each do |segment|
  442           segment == DOT_DOT ? resolved_segments.pop : resolved_segments << segment
  443         end
  444       end
  445     end
  446 
  447     if recheck
  448       target_path = join_path resolved_segments, jail_root
  449       if descends_from? target_path, jail
  450         target_path
  451       elsif opts.fetch :recover, true
  452         logger.warn %(#{opts[:target_name] || 'path'} is outside of jail; recovering automatically)
  453         jail_segments, _ = partition_path jail unless jail_segments
  454         join_path jail_segments + target_segments, jail_root
  455       else
  456         raise ::SecurityError, %(#{opts[:target_name] || 'path'} #{target} is outside of jail: #{jail} (disallowed in safe mode))
  457       end
  458     else
  459       join_path resolved_segments, jail_root
  460     end
  461   end
  462 
  463   # Public: Resolve a web path from the target and start paths.
  464   # The main function of this operation is to resolve any parent
  465   # references and remove any self references.
  466   #
  467   # The target is assumed to be a path, not a qualified URI.
  468   # That check should happen before this method is invoked.
  469   #
  470   # target - the String target path
  471   # start  - the String start (i.e., parent) path
  472   #
  473   # returns a String path that joins the target path with the
  474   # start path with any parent references resolved and self
  475   # references removed
  476   def web_path target, start = nil
  477     target = posixify target
  478     start = posixify start
  479 
  480     unless start.nil_or_empty? || (web_root? target)
  481       target, uri_prefix = extract_uri_prefix %(#{start}#{(start.end_with? SLASH) ? '' : SLASH}#{target})
  482     end
  483 
  484     # use this logic instead if we want to normalize target if it contains a URI
  485     #unless web_root? target
  486     #  target, uri_prefix = extract_uri_prefix target if preserve_uri_target
  487     #  target, uri_prefix = extract_uri_prefix %(#{start}#{SLASH}#{target}) unless uri_prefix || start.nil_or_empty?
  488     #end
  489 
  490     target_segments, target_root = partition_path target, true
  491     resolved_segments = []
  492     target_segments.each do |segment|
  493       if segment == DOT_DOT
  494         if resolved_segments.empty?
  495           resolved_segments << segment unless target_root && target_root != DOT_SLASH
  496         elsif resolved_segments[-1] == DOT_DOT
  497           resolved_segments << segment
  498         else
  499           resolved_segments.pop
  500         end
  501       else
  502         resolved_segments << segment
  503         # checking for empty would eliminate repeating forward slashes
  504         #resolved_segments << segment unless segment.empty?
  505       end
  506     end
  507 
  508     if (resolved_path = join_path resolved_segments, target_root).include? ' '
  509       resolved_path = resolved_path.gsub ' ', '%20'
  510     end
  511 
  512     uri_prefix ? %(#{uri_prefix}#{resolved_path}) : resolved_path
  513   end
  514 
  515   private
  516 
  517   # Internal: Efficiently extracts the URI prefix from the specified String if the String is a URI
  518   #
  519   # Uses the Asciidoctor::UriSniffRx regex to match the URI prefix in the specified String (e.g., http://). If present,
  520   # the prefix is removed.
  521   #
  522   # str - the String to check
  523   #
  524   # returns a tuple containing the specified string without the URI prefix, if present, and the extracted URI prefix.
  525   def extract_uri_prefix str
  526     if (str.include? ':') && UriSniffRx =~ str
  527       [(str.slice $&.length, str.length), $&]
  528     else
  529       str
  530     end
  531   end
  532 end
  533 end