about summary refs log tree commit diff
path: root/lib/tasks
diff options
context:
space:
mode:
authorClaire <claire.github-309c@sitedethib.com>2023-01-11 21:53:11 +0100
committerGitHub <noreply@github.com>2023-01-11 21:53:11 +0100
commita65f86ae5596d9c51a76cb05a3ebf5cd965df6ef (patch)
treeff1c0e9976a07eec15b08fd10bc783be7a99845c /lib/tasks
parent2ba14097ffb377d1a07fb08612ba3d7727ae437e (diff)
Fix `$` not being escaped in `.env.production` file generated by `mastodon:setup` (#23012)
* Fix `$` not being escaped in `.env.production` file generated by `mastodon:setup`

* Improve robustness of dotenv escaping
Diffstat (limited to 'lib/tasks')
-rw-r--r--lib/tasks/mastodon.rake61
1 files changed, 50 insertions, 11 deletions
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index 3c891a07f..cd6d1bcab 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -395,18 +395,11 @@ namespace :mastodon do
         incompatible_syntax = false
 
         env_contents = env.each_pair.map do |key, value|
-          if value.is_a?(String) && value =~ /[\s\#\\"]/
-            incompatible_syntax = true
+          value = value.to_s
+          escaped = dotenv_escape(value)
+          incompatible_syntax = true if value != escaped
 
-            if value =~ /[']/
-              value = value.to_s.gsub(/[\\"\$]/) { |x| "\\#{x}" }
-              "#{key}=\"#{value}\""
-            else
-              "#{key}='#{value}'"
-            end
-          else
-            "#{key}=#{value}"
-          end
+          escaped
         end.join("\n")
 
         generated_header = "# Generated with mastodon:setup on #{Time.now.utc}\n\n".dup
@@ -519,3 +512,49 @@ def disable_log_stdout!
   HttpLog.configuration.logger = dev_null
   Paperclip.options[:log]      = false
 end
+
+def dotenv_escape(value)
+  # Dotenv has its own parser, which unfortunately deviates somewhat from
+  # what shells actually do.
+  #
+  # In particular, we can't use Shellwords::escape because it outputs a
+  # non-quotable string, while Dotenv requires `#` to always be in quoted
+  # strings.
+  #
+  # Therefore, we need to write our own escape code…
+  # Dotenv's parser has a *lot* of edge cases, and I think not every
+  # ASCII string can even be represented into something Dotenv can parse,
+  # so this is a best effort thing.
+  #
+  # In particular, strings with all the following probably cannot be
+  # escaped:
+  # - `#`, or ends with spaces, which requires some form of quoting (simply escaping won't work)
+  # - `'` (single quote), preventing us from single-quoting
+  # - `\` followed by either `r` or `n`
+
+  # No character that would cause Dotenv trouble
+  return value unless /[\s\#\\"'$]/.match?(value)
+
+  # As long as the value doesn't include single quotes, we can safely
+  # rely on single quotes
+  return "'#{value}'" unless /[']/.match?(value)
+
+  # If the value contains the string '\n' or '\r' we simply can't use
+  # a double-quoted string, because Dotenv will expand \n or \r no
+  # matter how much escaping we add.
+  double_quoting_disallowed = /\\[rn]/.match?(value)
+
+  value = value.gsub(double_quoting_disallowed ? /[\\"'\s]/ : /[\\"']/) { |x| "\\#{x}" }
+
+  # Dotenv is especially tricky with `$` as unbalanced
+  # parenthesis will make it not unescape `\$` as `$`…
+
+  # Variables
+  value = value.gsub(/\$(?!\()/) { |x| "\\#{x}" }
+  # Commands
+  value = value.gsub(/\$(?<cmd>\((?:[^()]|\g<cmd>)+\))/) { |x| "\\#{x}" }
+
+  value = "\"#{value}\"" unless double_quoting_disallowed
+
+  value
+end