Skip to content

Commit

Permalink
Separate command line building and sanitizing into its own class.
Browse files Browse the repository at this point in the history
  • Loading branch information
Fryguy committed Jul 3, 2014
1 parent 0f56ab5 commit e524f85
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 117 deletions.
53 changes: 3 additions & 50 deletions lib/awesome_spawn.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
require "awesome_spawn/version"
require "awesome_spawn/command_line_builder"
require "awesome_spawn/command_result"
require "awesome_spawn/command_result_error"
require "awesome_spawn/no_such_file_error"

require "shellwords"
require "open3"

module AwesomeSpawn
Expand Down Expand Up @@ -100,60 +100,13 @@ def run!(command, options = {})
command_result
end

# Build the full command line.
#
# @param [String] command The command to run
# @param [Hash,Array] params Optional command line parameters. They can
# be passed as a Hash or associative Array. The values are sanitized to
# prevent command line injection. Keys as symbols are prefixed with `--`,
# and `_` is replaced with `-`.
#
# - `{:key => "value"}` generates `--key value`
# - `{"--key" => "value"}` generates `--key value`
# - `{:key= => "value"}` generates `--key=value`
# - `{"--key=" => "value"}` generates `--key=value`
# - `{:key_name => "value"}` generates `--key-name value`
# - `{:key => nil}` generates `--key`
# - `{"-f" => ["file1", "file2"]}` generates `-f file1 file2`
# - `{nil => ["file1", "file2"]}` generates `file1 file2`
#
# @return [String] The full command line
# (see CommandLineBuilder#build)
def build_command_line(command, params = nil)
return command.to_s if params.nil? || params.empty?
"#{command} #{assemble_params(sanitize(params))}"
CommandLineBuilder.new.build(command, params)
end

private

def sanitize(params)
return [] if params.nil? || params.empty?
params.collect do |k, v|
[sanitize_key(k), sanitize_value(v)]
end
end

def sanitize_key(key)
case key
when Symbol then "--#{key.to_s.tr("_", "-")}"
else key
end
end

def sanitize_value(value)
case value
when Array then value.collect { |i| i.to_s.shellescape }
when NilClass then value
else value.to_s.shellescape
end
end

def assemble_params(sanitized_params)
sanitized_params.collect do |pair|
pair_joiner = pair.first.to_s.end_with?("=") ? "" : " "
pair.flatten.compact.join(pair_joiner)
end.join(" ")
end

def launch(command, in_data, spawn_options = {})
spawn_options = spawn_options.merge(:stdin_data => in_data) if in_data
output, error, status = Open3.capture3(command, spawn_options)
Expand Down
59 changes: 59 additions & 0 deletions lib/awesome_spawn/command_line_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
require "shellwords"

module AwesomeSpawn
class CommandLineBuilder
# Build the full command line.
#
# @param [String] command The command to run
# @param [Hash,Array] params Optional command line parameters. They can
# be passed as a Hash or associative Array. The values are sanitized to
# prevent command line injection. Keys as symbols are prefixed with `--`,
# and `_` is replaced with `-`.
#
# - `{:key => "value"}` generates `--key value`
# - `{"--key" => "value"}` generates `--key value`
# - `{:key= => "value"}` generates `--key=value`
# - `{"--key=" => "value"}` generates `--key=value`
# - `{:key_name => "value"}` generates `--key-name value`
# - `{:key => nil}` generates `--key`
# - `{"-f" => ["file1", "file2"]}` generates `-f file1 file2`
# - `{nil => ["file1", "file2"]}` generates `file1 file2`
#
# @return [String] The full command line
def build(command, params = nil)
return command.to_s if params.nil? || params.empty?
"#{command} #{assemble_params(sanitize(params))}"
end

private

def sanitize(params)
return [] if params.nil? || params.empty?
params.collect do |k, v|
[sanitize_key(k), sanitize_value(v)]
end
end

def sanitize_key(key)
case key
when Symbol then "--#{key.to_s.tr("_", "-")}"
else key
end
end

def sanitize_value(value)
case value
when Array then value.collect { |i| i.to_s.shellescape }
when NilClass then value
else value.to_s.shellescape
end
end

def assemble_params(sanitized_params)
sanitized_params.collect do |pair|
pair_joiner = pair.first.to_s.end_with?("=") ? "" : " "
pair.flatten.compact.join(pair_joiner)
end.join(" ")
end
end
end
67 changes: 0 additions & 67 deletions spec/awesome_spawn_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,71 +128,4 @@
let(:run_method) {"run!"}
end
end

context ".build_command_line" do
it "sanitizes crazy params" do
cl = subject.build_command_line("true", modified_params)
expect(cl).to eq "true --user bob --pass P@\\$sw0\\^\\&\\ \\|\\<\\>/-\\+\\*d\\% --db --desc=Some\\ Description --symkey --symkey-dash pkg1 some\\ pkg --pool 123 --pool 456"
end

it "handles Symbol keys" do
cl = subject.build_command_line("true", :abc => "def")
expect(cl).to eq "true --abc def"
end

it "handles Symbol keys with tailing '='" do
cl = subject.build_command_line("true", :abc= => "def")
expect(cl).to eq "true --abc=def"
end

it "handles Symbol keys with underscore" do
cl = subject.build_command_line("true", :abc_def => "ghi")
expect(cl).to eq "true --abc-def ghi"
end

it "handles Symbol keys with underscore and tailing '='" do
cl = subject.build_command_line("true", :abc_def= => "ghi")
expect(cl).to eq "true --abc-def=ghi"
end

it "sanitizes Fixnum array param value" do
cl = subject.build_command_line("true", nil => [1])
expect(cl).to eq "true 1"
end

it "sanitizes Pathname param value" do
cl = subject.build_command_line("true", nil => [Pathname.new("/usr/bin/ruby")])
expect(cl).to eq "true /usr/bin/ruby"
end

it "sanitizes Pathname param key" do
cl = subject.build_command_line("true", Pathname.new("/usr/bin/ruby") => nil)
expect(cl).to eq "true /usr/bin/ruby"
end

it "with params as empty Hash" do
cl = subject.build_command_line("true", {})
expect(cl).to eq "true"
end

it "with params as nil" do
cl = subject.build_command_line("true", nil)
expect(cl).to eq "true"
end

it "without params" do
cl = subject.build_command_line("true")
expect(cl).to eq "true"
end

it "with Pathname command" do
cl = subject.build_command_line(Pathname.new("/usr/bin/ruby"))
expect(cl).to eq "/usr/bin/ruby"
end

it "with Pathname command and params" do
cl = subject.build_command_line(Pathname.new("/usr/bin/ruby"), "-v" => nil)
expect(cl).to eq "/usr/bin/ruby -v"
end
end
end
88 changes: 88 additions & 0 deletions spec/command_line_builder_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
require 'spec_helper'

describe AwesomeSpawn::CommandLineBuilder do
subject { described_class.new }

let(:params) do
{
"--user" => "bob",
"--pass" => "P@$sw0^& |<>/-+*d%",
"--db" => nil,
"--desc=" => "Some Description",
:symkey => nil,
:symkey_dash => nil,
nil => ["pkg1", "some pkg"]
}
end

let (:modified_params) do
params.to_a + [123, 456].collect {|pool| ["--pool", pool]}
end

context "#build" do
it "sanitizes crazy params" do
cl = subject.build("true", modified_params)
expect(cl).to eq "true --user bob --pass P@\\$sw0\\^\\&\\ \\|\\<\\>/-\\+\\*d\\% --db --desc=Some\\ Description --symkey --symkey-dash pkg1 some\\ pkg --pool 123 --pool 456"
end

it "handles Symbol keys" do
cl = subject.build("true", :abc => "def")
expect(cl).to eq "true --abc def"
end

it "handles Symbol keys with tailing '='" do
cl = subject.build("true", :abc= => "def")
expect(cl).to eq "true --abc=def"
end

it "handles Symbol keys with underscore" do
cl = subject.build("true", :abc_def => "ghi")
expect(cl).to eq "true --abc-def ghi"
end

it "handles Symbol keys with underscore and tailing '='" do
cl = subject.build("true", :abc_def= => "ghi")
expect(cl).to eq "true --abc-def=ghi"
end

it "sanitizes Fixnum array param value" do
cl = subject.build("true", nil => [1])
expect(cl).to eq "true 1"
end

it "sanitizes Pathname param value" do
cl = subject.build("true", nil => [Pathname.new("/usr/bin/ruby")])
expect(cl).to eq "true /usr/bin/ruby"
end

it "sanitizes Pathname param key" do
cl = subject.build("true", Pathname.new("/usr/bin/ruby") => nil)
expect(cl).to eq "true /usr/bin/ruby"
end

it "with params as empty Hash" do
cl = subject.build("true", {})
expect(cl).to eq "true"
end

it "with params as nil" do
cl = subject.build("true", nil)
expect(cl).to eq "true"
end

it "without params" do
cl = subject.build("true")
expect(cl).to eq "true"
end

it "with Pathname command" do
cl = subject.build(Pathname.new("/usr/bin/ruby"))
expect(cl).to eq "/usr/bin/ruby"
end

it "with Pathname command and params" do
cl = subject.build(Pathname.new("/usr/bin/ruby"), "-v" => nil)
expect(cl).to eq "/usr/bin/ruby -v"
end
end
end

0 comments on commit e524f85

Please sign in to comment.