Determine if one path is under another path in Ruby? - ruby

I have two paths: application root path and target path. What is the simplest way to ensure that the target path is the children of application root path?
Basically the target path provided by the user is to be displayed by my server. But I want to constrain my server so only the files under the application root path are displayable. So I want to check that the target path is under the root path.
The root path can contain nested directories.

Another way:
def child?(root, target)
raise ArgumentError, "target.size=#{target.size} < #{root.size} = root.size"\
if target.size < root.size
target[0...root.size] == root &&
(target.size == root.size || target[root.size] == ?/)
end
root = "/root/app"
p child?(root, "/root/app/some/path") # => true
p child?(root, "/root/apple") # => false
p child?(root, "/root/app") # => true
p child?(root, "/root") # => ArgumentError: target.size = 5 < 9 = root.size

Perhaps a bit inefficient, but foolproof
require 'find'
Find.find(root).include?(target)

You haven't provided any use case, thus I assume the following variables apply to your case
root = "/var/www/"
target = "/var/www/logs/something/else.log"
You can use a regular expression or even more simple String#start_with?
target.start_with?(root)

How about a regular expression:
root = "/root/app/"
target = "/root/app/some/path"
target =~ /^#{root}/
You could use Pathname#realdirpath to convert relative paths to absolute paths if needed.

I like Pathname#ascend for this:
ancestor = Pathnew.new(path1).realdirpath
descendant = Pathnew.new(path2).realdirpath
descendant.ascend { |path| break true if path == ancestor } || false
(Pathname#== works by string comparison, so it helps to sanitize your inputs with #realdirpath first.)

To avoid path injection you can use File.path method:
[21] pry(main)> path_1 = File.path(Rails.root.join('public', '../config/routes.rb') )
=> "/project_path/config/routes.rb"
[22] pry(main)> path_2 = File.path(Rails.root.join('public', 'robots.txt') )
=> "/project_path/public/robots.txt"
[24] pry(main)> path_1.include?(Rails.root.join('public').to_s )
=> false
[25] pry(main)> path_2.include?(Rails.root.join('public').to_s )
=> true

Related

Find the relative depth of child folder

Given two paths, one parent and a child folder(or grandchild, grand-grandchild, etc), find the relative depth the child folder from the parent folder. It is always guaranteed that child folder is always present inside the parent folder and the paths always end with slash.
Example:
parent = "/home/user/path/parent/"
child = "/home/user/path/parent/g/g/child/"
#ans = 3
The most simple way to solve this will look like:
parent = '/home/user/path/parent/'
child = '/home/user/path/parent/g/g/child/'
# Split the paths
child_chunks = child.split(File::SEPARATOR).reject(&:empty?)
parent_chunks = parent.split(File::SEPARATOR).reject(&:empty?)
# Find the diff between the two lists
puts (child_chunks - parent_chunks).length # => 3
The better way though would be to use Pathname helper:
require 'pathname'
parent = Pathname.new('/home/user/path/parent/')
child = Pathname.new('/home/user/path/parent/g/g/child/')
puts child.relative_path_from(parent).to_s.split(File::SEPARATOR).length # => 3
def relative_depth(path1, path2)
path2.count(File::SEPARATOR) - path1.count(File::SEPARATOR)
end
path1 = 'a/b/c/'
path2 = 'a/b/c/d/e/f/'
relative_depth(path1, path2)
#=> 3
path3 = 'C:a\\b\\c\\'
path4 = 'C:a\\b\\c\\d\\e\\f\\'
File::SEPARATOR = "\\" # simulate Windows
relative_depth(path3, path4)
#=> 3
See String#count.
Demo

Review for ruby "if" statement

do you have any suggestions for making this if condition shorter (more elegant) somehow?
if (#path.start_with? "scp" || #path.start_with? "http")
#source = "url"
else
#source = "local"
end
What if I have few more prefix to check (let's say ftp1, ftp2, and ftp3)?
start_with? can take multiple strings as argument
#source = #path.start_with?("scp", "http") ? "url" : "local"
It might makes sense to extract the prefixes into an array first when the list gets longer:
URL_PREFIXES = %w[ scp http ]
I can think of serveral ways to use that URL_PREFIXES constant. For example I often prefer if...else blocks over one liner of readability reasons:
if URL_PREFIXES.any? { |p| #path.start_with?(p) }
#source = 'url'
else
#source = 'local'
end
Or this:
#source = 'local'
#source = 'url' if URL_PREFIXES.any? { |p| #path.start_with?(p) }
Or determine the prefix first:
prefix = #path.split(':').first
#source = URL_PREFIXES.include?(prefix) ? 'url' : 'local'
If you are working with Rails you might want to write this instead:
prefix = #path.split(':').first
#source = prefix.in?(URL_PREFIXES) ? 'url' : 'local'

Ruby, removing parts a file path

$local_path_to_css_file = File.expand_path(filename)
gives me
A/B/C/D/CSS/filename
or
A/B/C/D/CSS/layouts/filename
I want the result to be:
css/filename
or
css/layouts/filename
to remove everything up until css/.
You can use Pathname
require 'pathname'
absolute_path = Pathname.new(File.expand_path(filename))
project_root = Pathname.new("/A/B/C/D") # you can set up root somewhere else, e.g. at point where script starts
relative = absolute_path.relative_path_from(project_root)
relative.to_s # => "css/filename"
A look-behind pattern will match your need.
def my_path(s)
s[/(?=CSS).*/]
end
my_path "A/B/C/D/CSS/filename" # => CSS/filename

How do I safely join relative url segments?

I'm trying to find a robust method of joining partial url path segments together. Is there a quick way to do this?
I tried the following:
puts URI::join('resource/', '/edit', '12?option=test')
I expect:
resource/edit/12?option=test
But I get the error:
`merge': both URI are relative (URI::BadURIError)
I have used File.join() in the past for this but something does not seem right about using the file library for urls.
URI's api is not neccearily great.
URI::join will work only if the first one starts out as an absolute uri with protocol, and the later ones are relative in the right ways... except I try to do that and can't even get that to work.
This at least doesn't error, but why is it skipping the middle component?
URI::join('http://somewhere.com/resource', './edit', '12?option=test')
I think maybe URI just kind of sucks. It lacks significant api on instances, such as an instance #join or method to evaluate relative to a base uri, that you'd expect. It's just kinda crappy.
I think you're going to have to write it yourself. Or just use File.join and other File path methods, after testing all the edge cases you can think of to make sure it does what you want/expect.
edit 9 Dec 2016 I figured out the addressable gem does it very nicely.
base = Addressable::URI.parse("http://example.com")
base + "foo.html"
# => #<Addressable::URI:0x3ff9964aabe4 URI:http://example.com/foo.html>
base = Addressable::URI.parse("http://example.com/path/to/file.html")
base + "relative_file.xml"
# => #<Addressable::URI:0x3ff99648bc80 URI:http://example.com/path/to/relative_file.xml>
base = Addressable::URI.parse("https://example.com/path")
base + "//newhost/somewhere.jpg"
# => #<Addressable::URI:0x3ff9960c9ebc URI:https://newhost/somewhere.jpg>
base = Addressable::URI.parse("http://example.com/path/subpath/file.html")
base + "../up-one-level.html"
=> #<Addressable::URI:0x3fe13ec5e928 URI:http://example.com/path/up-one-level.html>
Have uri as URI::Generic or subclass of thereof
uri.path += '/123'
Enjoy!
06/25/2016 UPDATE for skeptical folk
require 'uri'
uri = URI('http://ioffe.net/boris')
uri.path += '/123'
p uri
Outputs
<URI::HTTP:0x2341a58 URL:http://ioffe.net/boris/123>
Run me
The problem is that resource/ is relative to the current directory, but /edit refers to the top level directory due to the leading slash. It's impossible to join the two directories without already knowing for certain that edit contains resource.
If you're looking for purely string operations, simply remove the leading or trailing slashes from all parts, then join them with / as the glue.
The way to do it using URI.join is:
URI.join('http://example.com', '/foo/', 'bar')
Pay attention to the trailing slashes. You can find the complete documentation here:
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/uri/rdoc/URI.html#method-c-join
As you noticed, URI::join won't combine paths with repeated slashes, so it doesn't fit the part.
Turns out it doesn't require a lot of Ruby code to achieve this:
module GluePath
def self.join(*paths, separator: '/')
paths = paths.compact.reject(&:empty?)
last = paths.length - 1
paths.each_with_index.map { |path, index|
_expand(path, index, last, separator)
}.join
end
def self._expand(path, current, last, separator)
if path.start_with?(separator) && current != 0
path = path[1..-1]
end
unless path.end_with?(separator) || current == last
path = [path, separator]
end
path
end
end
The algorithm takes care of consecutive slashes, preserves start and end slashes, and ignores nil and empty strings.
puts GluePath::join('resource/', '/edit', '12?option=test')
outputs
resource/edit/12?option=test
Use this code:
File.join('resource/', '/edit', '12?option=test').
gsub(File::SEPARATOR, '/').
sub(/^\//, '')
# => resource/edit/12?option=test
example with empty strings:
File.join('', '/edit', '12?option=test').
gsub(File::SEPARATOR, '/').
sub(/^\//, '')
# => edit/12?option=test
Or use this if possible to use segments like resource/, edit/, 12?option=test and where http: is only a placeholder to get a valid URI. This works for me.
URI.
join('http:', 'resource/', 'edit/', '12?option=test').
path.
sub(/^\//, '')
# => "resource/edit/12"
A not optimized solution. Note that it doesn't take query params into account. It only handles paths.
class URL
def self.join(*str)
str.map { |path|
new_path = path
# Check the first character
if path[0] == "/"
new_path = new_path[1..-1]
end
# Check the last character
if path[-1] != "/"
new_path += "/"
end
new_path
}.join
end
end
This question is nearly a decade old, yet it seems that there is no perfect solution posted.
A handful of posted answers fail to handle multiple //, e.g. stuff like path = path[1..-1] if path.start_with?('/')
Answers that simply call File.join(*paths) seem to be the accepted "Ruby way," yet they fail in cases where you pass a URI object, e.g. File.join(URI.join('some/path')) fails with TypeError: no implicit conversion of URI::Generic into String.
Below is what I ended up using:
module UrlHelper
def self.join(*paths)
# yes, Ruby's stdlib really does lack a functional join method for URLs
File.join(*paths.map(&:to_s))
end
end
You can use File.join('resource/', '/edit', '12?option=test')
I improved #Maximo Mussini's script to make it works gracefully:
SmartURI.join('http://example.com/subpath', 'hello', query: { token: secret })
=> "http://example.com/subpath/hello?token=secret"
https://gist.github.com/zernel/0f10c71f5a9e044653c1a65c6c5ad697
require 'uri'
module SmartURI
SEPARATOR = '/'
def self.join(*paths, query: nil)
paths = paths.compact.reject(&:empty?)
last = paths.length - 1
url = paths.each_with_index.map { |path, index|
_expand(path, index, last)
}.join
if query.nil?
return url
elsif query.is_a? Hash
return url + "?#{URI.encode_www_form(query.to_a)}"
else
raise "Unexpected input type for query: #{query}, it should be a hash."
end
end
def self._expand(path, current, last)
if path.starts_with?(SEPARATOR) && current != 0
path = path[1..-1]
end
unless path.ends_with?(SEPARATOR) || current == last
path = [path, SEPARATOR]
end
path
end
end
You can use this:
URI.join('http://exemple.com', '/a/', 'b/', 'c/', 'd')
=> #<URI::HTTP http://exemple.com/a/b/c/d>
URI.join('http://exemple.com', '/a/', 'b/', 'c/', 'd').to_s
=> "http://exemple.com/a/b/c/d"
See: http://ruby-doc.org/stdlib-2.4.1/libdoc/uri/rdoc/URI.html#method-c-join-label-Synopsis
My understanding of URI::join is that it thinks like a web browser does.
To evaluate it, point your mental web browser to the first parameter, and keep clicking links until you browse to the last parameter.
For example, URI::join('http://example.com/resource/', '/edit', '12?option=test'), you would browse like this:
http://example.com/resource/, click a link to /edit (a file at the root of the site)
http://example.com/edit, click a link to 12?option=test (a file in the same directory as edit)
http://example.com/12?option=test
If the first link were /edit/ (with a trailing slash), or /edit/foo, then the next link would be relative to /edit/ rather than /.
This page possibly explains it better than I can: Why is URI.join so counterintuitive?
This is my simple take on this problem, just splitting up all the path segments and join them together again. This only works if you're only working with relative path segments, but if that's all you want to do this is handy.
def join_paths *paths
paths.map{|p| p.split('/')}
.flatten
.reject(&:empty?)
.compact
.join('/')
end
Then you can use it like so:
join_paths 'foo/', '/bar', 'a/b/c', 'd' #=> "foo/bar/a/b/c/d"

How to check if a directory/file/symlink exists with one command in Ruby

Is there a single way of detecting if a directory/file/symlink/etc. entity (more generalized) exists?
I need a single function because I need to check an array of paths that could be directories, files or symlinks. I know File.exists?"file_path" works for directories and files but not for symlinks (which is File.symlink?"symlink_path").
The standard File module has the usual file tests available:
RUBY_VERSION # => "1.9.2"
bashrc = ENV['HOME'] + '/.bashrc'
File.exist?(bashrc) # => true
File.file?(bashrc) # => true
File.directory?(bashrc) # => false
You should be able to find what you want there.
OP: "Thanks but I need all three true or false"
Obviously not. Ok, try something like:
def file_dir_or_symlink_exists?(path_to_file)
File.exist?(path_to_file) || File.symlink?(path_to_file)
end
file_dir_or_symlink_exists?(bashrc) # => true
file_dir_or_symlink_exists?('/Users') # => true
file_dir_or_symlink_exists?('/usr/bin/ruby') # => true
file_dir_or_symlink_exists?('some/bogus/path/to/a/black/hole') # => false
Why not define your own function File.exists?(path) or File.symlink?(path) and use that?
Just File.exist? on it's own will take care of all of the above for you

Resources