From 15146b2b671b753da5befc4e9ea0a4b3c67ae1f0 Mon Sep 17 00:00:00 2001 From: Ruoyu Zhong Date: Wed, 4 Dec 2024 02:45:09 +0800 Subject: [PATCH 1/3] utils/service: add `systemd_quote` helper We need a way to escape systemd command lines properly as systemd treats unrecognised escape sequences as separate literal characters. This helper function does that. --- Library/Homebrew/test/utils/service_spec.rb | 24 ++++++++++++++ Library/Homebrew/utils/service.rb | 35 +++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 Library/Homebrew/test/utils/service_spec.rb diff --git a/Library/Homebrew/test/utils/service_spec.rb b/Library/Homebrew/test/utils/service_spec.rb new file mode 100644 index 0000000000000..c09c6cf828a08 --- /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 3c13d75c70ce0..5a3ff05ca1fa1 100644 --- a/Library/Homebrew/utils/service.rb +++ b/Library/Homebrew/utils/service.rb @@ -48,5 +48,40 @@ def self.launchctl? 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" + "\\a" + when "\b" + "\\b" + when "\f" + "\\f" + when "\n" + "\\n" + when "\r" + "\\r" + when "\t" + "\\t" + when "\v" + "\\v" + when "\\" + "\\\\" + when "\"" + "\\\"" + else + char + end + end + result << "\"" + result.freeze + end end end From bba67ec02ef5edace8677938e1b414111f1abc31 Mon Sep 17 00:00:00 2001 From: Ruoyu Zhong Date: Wed, 4 Dec 2024 02:51:26 +0800 Subject: [PATCH 2/3] service: fix systemd command line quoting Fixes #18802 (affected formulae still need rebuilding). --- Library/Homebrew/service.rb | 2 +- Library/Homebrew/test/service_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Library/Homebrew/service.rb b/Library/Homebrew/service.rb index e4f27fd73c0ed..883f19bf6d06d 100644 --- a/Library/Homebrew/service.rb +++ b/Library/Homebrew/service.rb @@ -465,7 +465,7 @@ def to_systemd_unit EOS # 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 a4b7a913a97c0..6cb6c80f2b1d4 100644 --- a/Library/Homebrew/test/service_spec.rb +++ b/Library/Homebrew/test/service_spec.rb @@ -727,7 +727,7 @@ def stub_formula_with_service_sockets(sockets_var) [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 @@ def stub_formula_with_service_sockets(sockets_var) [Service] Type=oneshot - ExecStart=#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd + ExecStart="#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd" EOS expect(unit).to eq(unit_expect.strip) end @@ -783,7 +783,7 @@ def stub_formula_with_service_sockets(sockets_var) [Service] Type=simple - ExecStart=#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd + ExecStart="#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd" WorkingDirectory=#{Dir.home} EOS expect(unit).to eq(unit_expect.strip) From a8a6a5bdb7aefef147f59611a460fdc1d6899b23 Mon Sep 17 00:00:00 2001 From: Ruoyu Zhong Date: Wed, 4 Dec 2024 03:09:11 +0800 Subject: [PATCH 3/3] utils/service: simplify --- Library/Homebrew/utils/service.rb | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/Library/Homebrew/utils/service.rb b/Library/Homebrew/utils/service.rb index 5a3ff05ca1fa1..f682b19f21ea0 100644 --- a/Library/Homebrew/utils/service.rb +++ b/Library/Homebrew/utils/service.rb @@ -58,26 +58,16 @@ def self.systemd_quote(str) # quoting the entire string. str.each_char do |char| result << case char - when "\a" - "\\a" - when "\b" - "\\b" - when "\f" - "\\f" - when "\n" - "\\n" - when "\r" - "\\r" - when "\t" - "\\t" - when "\v" - "\\v" - when "\\" - "\\\\" - when "\"" - "\\\"" - else - 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 << "\""