diff --git a/Library/Homebrew/service.rb b/Library/Homebrew/service.rb index 2ef8c421f4..48fd90270c 100644 --- a/Library/Homebrew/service.rb +++ b/Library/Homebrew/service.rb @@ -455,7 +455,7 @@ module Homebrew sig { returns(String) } def to_systemd_unit # command needs to be first because it initializes all other values - cmd = command&.map { |arg| Utils::Shell.sh_quote(arg) } + cmd = command&.map { |arg| Utils::Service.systemd_quote(arg) } &.join(" ") options = [] diff --git a/Library/Homebrew/test/service_spec.rb b/Library/Homebrew/test/service_spec.rb index f280b1981d..b648094b47 100644 --- a/Library/Homebrew/test/service_spec.rb +++ b/Library/Homebrew/test/service_spec.rb @@ -727,7 +727,7 @@ RSpec.describe Homebrew::Service do [Service] Type=simple - ExecStart=#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd test + ExecStart="#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd" "test" Restart=always RestartSec=30 WorkingDirectory=#{HOMEBREW_PREFIX}/var @@ -760,7 +760,7 @@ RSpec.describe Homebrew::Service do [Service] Type=oneshot - ExecStart=#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd + ExecStart="#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd" SYSTEMD expect(unit).to eq(unit_expect) end @@ -783,7 +783,7 @@ RSpec.describe Homebrew::Service do [Service] Type=simple - ExecStart=#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd + ExecStart="#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd" WorkingDirectory=#{Dir.home} SYSTEMD expect(unit).to eq(unit_expect) diff --git a/Library/Homebrew/test/utils/service_spec.rb b/Library/Homebrew/test/utils/service_spec.rb new file mode 100644 index 0000000000..c09c6cf828 --- /dev/null +++ b/Library/Homebrew/test/utils/service_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "utils/service" + +RSpec.describe Utils::Service do + describe "::systemd_quote" do + it "quotes empty strings correctly" do + expect(described_class.systemd_quote("")).to eq '""' + end + + it "quotes strings with special characters escaped correctly" do + expect(described_class.systemd_quote("\a\b\f\n\r\t\v\\")) + .to eq '"\\a\\b\\f\\n\\r\\t\\v\\\\"' + expect(described_class.systemd_quote("\"' ")).to eq "\"\\\"' \"" + end + + it "does not escape characters that do not need escaping" do + expect(described_class.systemd_quote("daemon off;")).to eq '"daemon off;"' + expect(described_class.systemd_quote("--timeout=3")).to eq '"--timeout=3"' + expect(described_class.systemd_quote("--answer=foo bar")) + .to eq '"--answer=foo bar"' + end + end +end diff --git a/Library/Homebrew/utils/service.rb b/Library/Homebrew/utils/service.rb index 3c13d75c70..f682b19f21 100644 --- a/Library/Homebrew/utils/service.rb +++ b/Library/Homebrew/utils/service.rb @@ -48,5 +48,30 @@ module Utils def self.systemctl? !systemctl.nil? end + + # Quote a string for use in systemd command lines, e.g., in `ExecStart`. + # https://www.freedesktop.org/software/systemd/man/latest/systemd.syntax.html#Quoting + sig { params(str: String).returns(String) } + def self.systemd_quote(str) + result = +"\"" + # No need to escape single quotes and spaces, as we're always double + # quoting the entire string. + str.each_char do |char| + result << case char + when "\a" then "\\a" + when "\b" then "\\b" + when "\f" then "\\f" + when "\n" then "\\n" + when "\r" then "\\r" + when "\t" then "\\t" + when "\v" then "\\v" + when "\\" then "\\\\" + when "\"" then "\\\"" + else char + end + end + result << "\"" + result.freeze + end end end