Repository: aksiksi/compose2nix
Pull request #35 (2024-08-23 14:43:29)

fix: properly escape Nix strings everywhere
Fix an issue where special characters were not properly escaped in template, leading to Nix parsing errors.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aksiksi/compose2nix/pull/35?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Assil+Ksiksi) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 71.46%. Comparing base [(`3f3f499`)](https://app.codecov.io/gh/aksiksi/compose2nix/commit/3f3f4996822a767ca06ed2418a6a10d18e1296e0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Assil+Ksiksi) to head [(`da395f8`)](https://app.codecov.io/gh/aksiksi/compose2nix/commit/da395f848da3decc224da70658e3466de8b9eec6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Assil+Ksiksi).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main      #35      +/-   ##
==========================================
- Coverage   72.58%   71.46%   -1.12%     
==========================================
  Files           6        6              
  Lines         704      708       +4     
==========================================
- Hits          511      506       -5     
- Misses        153      164      +11     
+ Partials       40       38       -2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aksiksi/compose2nix/pull/35?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Assil+Ksiksi).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Assil+Ksiksi).


Patch:
diff --git a/compose.go b/compose.go
index ae8d266..3d0600d 100644
--- a/compose.go
+++ b/compose.go
@@ -576,16 +576,6 @@ func (g *Generator) buildNixContainer(service types.ServiceConfig, networkMap ma
 				if err != nil {
 					return nil, fmt.Errorf("failed to convert healthcheck command: %w", err)
 				}
-
-				// We need to escape double-quotes for Nix.
-				//
-				// We also need to escape the special "${" sequence as it is possible that this is
-				// passed in to evaluate a Bash env variable as part of the command.
-				//
-				// See: https://nixos.org/manual/nix/stable/language/values
-				cmd = strings.ReplaceAll(cmd, `"`, `\"`)
-				cmd = strings.ReplaceAll(cmd, "${", `\${`)
-
 				c.ExtraOptions = append(c.ExtraOptions, fmt.Sprintf("--health-cmd=%s", cmd))
 			}
 			if timeout := healthCheck.Timeout; timeout != nil {
diff --git a/template.go b/template.go
index a11bdcd..bc6440d 100644
--- a/template.go
+++ b/template.go
@@ -28,7 +28,7 @@ func derefInt(v *int) int {
 func toNixValue(v any) any {
 	switch v := v.(type) {
 	case string:
-		return fmt.Sprintf("%q", v)
+		return fmt.Sprintf("%q", escapeNixString(v))
 	default:
 		return v
 	}
@@ -37,7 +37,7 @@ func toNixValue(v any) any {
 func toNixList(s []string) string {
 	b := strings.Builder{}
 	for i, e := range s {
-		b.WriteString(fmt.Sprintf("%q", e))
+		b.WriteString(fmt.Sprintf("%q", escapeNixString(e)))
 		if i < len(s)-1 {
 			b.WriteString(" ")
 		}
@@ -45,8 +45,25 @@ func toNixList(s []string) string {
 	return fmt.Sprintf("[ %s ]", b.String())
 }
 
+func escapeNixString(s string) string {
+	// https://nix.dev/manual/nix/latest/language/syntax#string-literal
+	s = strings.ReplaceAll(s, `\`, `\\`)
+	s = strings.ReplaceAll(s, `"`, `\"`)
+	s = strings.ReplaceAll(s, `${`, `\${`)
+	return s
+}
+
+func escapeIndentedNixString(s string) string {
+	// https://nix.dev/manual/nix/latest/language/syntax#string-literal
+	s = strings.ReplaceAll(s, `''`, `'''`)
+	s = strings.ReplaceAll(s, `$`, `''$`)
+	return s
+}
+
 var funcMap template.FuncMap = template.FuncMap{
-	"derefInt":   derefInt,
-	"toNixValue": toNixValue,
-	"toNixList":  toNixList,
+	"derefInt":                derefInt,
+	"toNixValue":              toNixValue,
+	"toNixList":               toNixList,
+	"escapeNixString":         escapeNixString,
+	"escapeIndentedNixString": escapeIndentedNixString,
 }
diff --git a/templates/container.nix.tmpl b/templates/container.nix.tmpl
index 552a485..4aaac0c 100644
--- a/templates/container.nix.tmpl
+++ b/templates/container.nix.tmpl
@@ -4,7 +4,7 @@ virtualisation.oci-containers.containers."{{.Name}}" = {
   {{- if .Environment}}
   environment = {
     {{- range $k, $v := .Environment}}
-    "{{$k}}" = "{{$v}}";
+    "{{$k}}" = "{{escapeNixString $v}}";
     {{- end}}
   };
   {{- end}}
@@ -40,7 +40,7 @@ virtualisation.oci-containers.containers."{{.Name}}" = {
   {{- if .Labels}}
   labels = {
     {{- range $k, $v := .Labels}}
-    "{{$k}}" = "{{$v}}";
+    "{{$k}}" = "{{escapeNixString $v}}";
     {{- end}}
   };
   {{- end}}
@@ -68,7 +68,7 @@ virtualisation.oci-containers.containers."{{.Name}}" = {
   {{- if .ExtraOptions}}
   extraOptions = [
     {{- range .ExtraOptions}}
-    "{{.}}"
+    "{{escapeNixString .}}"
     {{- end}}
   ];
   {{- end}}
diff --git a/templates/network.nix.tmpl b/templates/network.nix.tmpl
index ad9584e..23d4aed 100644
--- a/templates/network.nix.tmpl
+++ b/templates/network.nix.tmpl
@@ -6,7 +6,7 @@ systemd.services."{{.Runtime}}-network-{{.Name}}" = {
     ExecStop = "{{.Runtime}} network rm -f {{.Name}}";
   };
   script = ''
-    {{ .Command }}
+    {{escapeIndentedNixString .Command }}
   '';
   {{- if rootTarget}}
   {{- /* PartOf for stop/restart of root, WantedBy for start of root. */}}
diff --git a/templates/volume.nix.tmpl b/templates/volume.nix.tmpl
index 207cbfa..4a0eb58 100644
--- a/templates/volume.nix.tmpl
+++ b/templates/volume.nix.tmpl
@@ -15,7 +15,7 @@ systemd.services."{{.Runtime}}-volume-{{.Name}}" = {
   ];
   {{- end}}
   script = ''
-    {{ .Command }}
+    {{escapeIndentedNixString .Command }}
   '';
   {{- if rootTarget}}
   {{- /* PartOf for stop/restart of root, WantedBy for start of root. */}}


Test patch:
diff --git a/nix_test.go b/nix_test.go
index 742e278..7b3e959 100644
--- a/nix_test.go
+++ b/nix_test.go
@@ -319,6 +319,14 @@ func TestDeployDevices(t *testing.T) {
 	runSubtestsWithGenerator(t, g)
 }
 
+func TestEscapeChars(t *testing.T) {
+	composePath, _ := getPaths(t, false)
+	g := &Generator{
+		Inputs: []string{composePath},
+	}
+	runSubtestsWithGenerator(t, g)
+}
+
 func TestNoCreateRootTarget(t *testing.T) {
 	composePath, _ := getPaths(t, false)
 	g := &Generator{
diff --git a/nixos-test/docker-compose.nix b/nixos-test/docker-compose.nix
index ccbcc17..7314b0a 100644
--- a/nixos-test/docker-compose.nix
+++ b/nixos-test/docker-compose.nix
@@ -52,6 +52,7 @@
       "compose2nix.systemd.service.Restart" = "no";
       "compose2nix.systemd.service.RuntimeMaxSec" = "360";
       "compose2nix.systemd.unit.Description" = "This is the service-a container!";
+      "escape-me" = "\"hello\"";
     };
     log-driver = "journald";
     extraOptions = [
@@ -170,7 +171,7 @@
       ExecStop = "docker network rm -f myproject_something";
     };
     script = ''
-      docker network inspect myproject_something || docker network create myproject_something --subnet=192.168.8.0/24 --gateway=192.168.8.1 --label=test-label=okay
+      docker network inspect myproject_something || docker network create myproject_something --subnet=192.168.8.0/24 --gateway=192.168.8.1 --label=escape-me='''hello''' --label=test-label=okay
     '';
     partOf = [ "docker-compose-myproject-root.target" ];
     wantedBy = [ "docker-compose-myproject-root.target" ];
@@ -202,7 +203,7 @@
       "/mnt/media"
     ];
     script = ''
-      docker volume inspect storage || docker volume create storage --opt=device=/mnt/media --opt=o=bind --opt=type=none
+      docker volume inspect storage || docker volume create storage --opt=device=/mnt/media --opt=o=bind --opt=type=none --label=escape-me='''hello'''
     '';
     partOf = [ "docker-compose-myproject-root.target" ];
     wantedBy = [ "docker-compose-myproject-root.target" ];
diff --git a/nixos-test/docker-compose.yml b/nixos-test/docker-compose.yml
index d57cd61..a92344e 100644
--- a/nixos-test/docker-compose.yml
+++ b/nixos-test/docker-compose.yml
@@ -19,6 +19,7 @@ services:
       - 'compose2nix.systemd.service.Restart=no'
       - "compose2nix.systemd.service.RuntimeMaxSec=360"
       - "compose2nix.systemd.unit.Description=This is the service-a container!"
+      - "escape-me=\"hello\""
     restart: unless-stopped
   service-b:
     image: docker.io/library/nginx:stable-alpine-slim
@@ -51,6 +52,7 @@ networks:
           gateway: 192.168.8.1
     labels:
       - "test-label=okay"
+      - "escape-me=''hello''"
 
 volumes:
   storage:
@@ -59,6 +61,8 @@ volumes:
       type: none
       device: /mnt/media
       o: bind
+    labels:
+      - "escape-me=''hello''"
   books:
     driver_opts:
       type: none
diff --git a/nixos-test/podman-compose.nix b/nixos-test/podman-compose.nix
index d28a96f..fbcc4e5 100644
--- a/nixos-test/podman-compose.nix
+++ b/nixos-test/podman-compose.nix
@@ -57,6 +57,7 @@
       "compose2nix.systemd.service.Restart" = "no";
       "compose2nix.systemd.service.RuntimeMaxSec" = "360";
       "compose2nix.systemd.unit.Description" = "This is the service-a container!";
+      "escape-me" = "\"hello\"";
     };
     log-driver = "journald";
     extraOptions = [
@@ -172,7 +173,7 @@
       ExecStop = "podman network rm -f myproject_something";
     };
     script = ''
-      podman network inspect myproject_something || podman network create myproject_something --subnet=192.168.8.0/24 --gateway=192.168.8.1 --label=test-label=okay
+      podman network inspect myproject_something || podman network create myproject_something --subnet=192.168.8.0/24 --gateway=192.168.8.1 --label=escape-me='''hello''' --label=test-label=okay
     '';
     partOf = [ "podman-compose-myproject-root.target" ];
     wantedBy = [ "podman-compose-myproject-root.target" ];
@@ -204,7 +205,7 @@
       "/mnt/media"
     ];
     script = ''
-      podman volume inspect storage || podman volume create storage --opt=device=/mnt/media --opt=o=bind --opt=type=none
+      podman volume inspect storage || podman volume create storage --opt=device=/mnt/media --opt=o=bind --opt=type=none --label=escape-me='''hello'''
     '';
     partOf = [ "podman-compose-myproject-root.target" ];
     wantedBy = [ "podman-compose-myproject-root.target" ];
diff --git a/testdata/TestBasic.docker.nix b/testdata/TestBasic.docker.nix
index faddc4b..860d4c7 100644
--- a/testdata/TestBasic.docker.nix
+++ b/testdata/TestBasic.docker.nix
@@ -74,7 +74,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestBasic.podman.nix b/testdata/TestBasic.podman.nix
index 91d2398..b5ff201 100644
--- a/testdata/TestBasic.podman.nix
+++ b/testdata/TestBasic.podman.nix
@@ -77,7 +77,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestBasicAutoFormat.docker.nix b/testdata/TestBasicAutoFormat.docker.nix
index 334509a..16a4165 100644
--- a/testdata/TestBasicAutoFormat.docker.nix
+++ b/testdata/TestBasicAutoFormat.docker.nix
@@ -68,7 +68,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestBasicAutoFormat.podman.nix b/testdata/TestBasicAutoFormat.podman.nix
index 2743478..2941397 100644
--- a/testdata/TestBasicAutoFormat.podman.nix
+++ b/testdata/TestBasicAutoFormat.podman.nix
@@ -71,7 +71,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestEscapeChars.compose.yml b/testdata/TestEscapeChars.compose.yml
new file mode 100644
index 0000000..0bb231a
--- /dev/null
+++ b/testdata/TestEscapeChars.compose.yml
@@ -0,0 +1,26 @@
+name: "dovecot"
+services:
+  dovecot:
+    container_name: dovecot
+    image: dovecot
+    labels:
+      ofelia.enabled: "true"
+      ofelia.job-exec.dovecot_imapsync_runner.schedule: "@every 1m"
+      ofelia.job-exec.dovecot_imapsync_runner.no-overlap: "true"
+      ofelia.job-exec.dovecot_imapsync_runner.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu nobody /usr/local/bin/imapsync_runner.pl || exit 0\""
+      ofelia.job-exec.dovecot_trim_logs.schedule: "@every 1m"
+      ofelia.job-exec.dovecot_trim_logs.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/trim_logs.sh || exit 0\""
+    networks:
+      - abc
+    volumes:
+      - def:/path/to/path
+
+networks:
+  abc:
+    labels:
+      my-label: "\"some quoted string\""
+
+volumes:
+  def:
+    labels:
+      other-label: "\"another quota string\""
diff --git a/testdata/TestEscapeChars.docker.nix b/testdata/TestEscapeChars.docker.nix
new file mode 100644
index 0000000..49b593c
--- /dev/null
+++ b/testdata/TestEscapeChars.docker.nix
@@ -0,0 +1,83 @@
+{ pkgs, lib, ... }:
+
+{
+  # Runtime
+  virtualisation.docker = {
+    enable = true;
+    autoPrune.enable = true;
+  };
+  virtualisation.oci-containers.backend = "docker";
+
+  # Containers
+  virtualisation.oci-containers.containers."dovecot" = {
+    image = "dovecot";
+    volumes = [
+      "dovecot_def:/path/to/path:rw"
+    ];
+    labels = {
+      "ofelia.enabled" = "true";
+      "ofelia.job-exec.dovecot_imapsync_runner.command" = "/bin/bash -c \"[[ \${MASTER} == y ]] && /usr/local/bin/gosu nobody /usr/local/bin/imapsync_runner.pl || exit 0\"";
+      "ofelia.job-exec.dovecot_imapsync_runner.no-overlap" = "true";
+      "ofelia.job-exec.dovecot_imapsync_runner.schedule" = "@every 1m";
+      "ofelia.job-exec.dovecot_trim_logs.command" = "/bin/bash -c \"[[ \${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/trim_logs.sh || exit 0\"";
+      "ofelia.job-exec.dovecot_trim_logs.schedule" = "@every 1m";
+    };
+    log-driver = "journald";
+    autoStart = false;
+    extraOptions = [
+      "--network-alias=dovecot"
+      "--network=dovecot_abc"
+    ];
+  };
+  systemd.services."docker-dovecot" = {
+    serviceConfig = {
+      Restart = lib.mkOverride 90 "no";
+    };
+    after = [
+      "docker-network-dovecot_abc.service"
+      "docker-volume-dovecot_def.service"
+    ];
+    requires = [
+      "docker-network-dovecot_abc.service"
+      "docker-volume-dovecot_def.service"
+    ];
+  };
+
+  # Networks
+  systemd.services."docker-network-dovecot_abc" = {
+    path = [ pkgs.docker ];
+    serviceConfig = {
+      Type = "oneshot";
+      RemainAfterExit = true;
+      ExecStop = "docker network rm -f dovecot_abc";
+    };
+    script = ''
+      docker network inspect dovecot_abc || docker network create dovecot_abc --label=my-label="some quoted string"
+    '';
+    partOf = [ "docker-compose-dovecot-root.target" ];
+    wantedBy = [ "docker-compose-dovecot-root.target" ];
+  };
+
+  # Volumes
+  systemd.services."docker-volume-dovecot_def" = {
+    path = [ pkgs.docker ];
+    serviceConfig = {
+      Type = "oneshot";
+      RemainAfterExit = true;
+    };
+    script = ''
+      docker volume inspect dovecot_def || docker volume create dovecot_def --label=other-label="another quota string"
+    '';
+    partOf = [ "docker-compose-dovecot-root.target" ];
+    wantedBy = [ "docker-compose-dovecot-root.target" ];
+  };
+
+  # Root service
+  # When started, this will automatically create all resources and start
+  # the containers. When stopped, this will teardown all resources.
+  systemd.targets."docker-compose-dovecot-root" = {
+    unitConfig = {
+      Description = "Root target generated by compose2nix.";
+    };
+  };
+}
diff --git a/testdata/TestEscapeChars.podman.nix b/testdata/TestEscapeChars.podman.nix
new file mode 100644
index 0000000..9aab454
--- /dev/null
+++ b/testdata/TestEscapeChars.podman.nix
@@ -0,0 +1,88 @@
+{ pkgs, lib, ... }:
+
+{
+  # Runtime
+  virtualisation.podman = {
+    enable = true;
+    autoPrune.enable = true;
+    dockerCompat = true;
+    defaultNetwork.settings = {
+      # Required for container networking to be able to use names.
+      dns_enabled = true;
+    };
+  };
+  virtualisation.oci-containers.backend = "podman";
+
+  # Containers
+  virtualisation.oci-containers.containers."dovecot" = {
+    image = "dovecot";
+    volumes = [
+      "dovecot_def:/path/to/path:rw"
+    ];
+    labels = {
+      "ofelia.enabled" = "true";
+      "ofelia.job-exec.dovecot_imapsync_runner.command" = "/bin/bash -c \"[[ \${MASTER} == y ]] && /usr/local/bin/gosu nobody /usr/local/bin/imapsync_runner.pl || exit 0\"";
+      "ofelia.job-exec.dovecot_imapsync_runner.no-overlap" = "true";
+      "ofelia.job-exec.dovecot_imapsync_runner.schedule" = "@every 1m";
+      "ofelia.job-exec.dovecot_trim_logs.command" = "/bin/bash -c \"[[ \${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/trim_logs.sh || exit 0\"";
+      "ofelia.job-exec.dovecot_trim_logs.schedule" = "@every 1m";
+    };
+    log-driver = "journald";
+    autoStart = false;
+    extraOptions = [
+      "--network-alias=dovecot"
+      "--network=dovecot_abc"
+    ];
+  };
+  systemd.services."podman-dovecot" = {
+    serviceConfig = {
+      Restart = lib.mkOverride 90 "no";
+    };
+    after = [
+      "podman-network-dovecot_abc.service"
+      "podman-volume-dovecot_def.service"
+    ];
+    requires = [
+      "podman-network-dovecot_abc.service"
+      "podman-volume-dovecot_def.service"
+    ];
+  };
+
+  # Networks
+  systemd.services."podman-network-dovecot_abc" = {
+    path = [ pkgs.podman ];
+    serviceConfig = {
+      Type = "oneshot";
+      RemainAfterExit = true;
+      ExecStop = "podman network rm -f dovecot_abc";
+    };
+    script = ''
+      podman network inspect dovecot_abc || podman network create dovecot_abc --label=my-label="some quoted string"
+    '';
+    partOf = [ "podman-compose-dovecot-root.target" ];
+    wantedBy = [ "podman-compose-dovecot-root.target" ];
+  };
+
+  # Volumes
+  systemd.services."podman-volume-dovecot_def" = {
+    path = [ pkgs.podman ];
+    serviceConfig = {
+      Type = "oneshot";
+      RemainAfterExit = true;
+    };
+    script = ''
+      podman volume inspect dovecot_def || podman volume create dovecot_def --label=other-label="another quota string"
+    '';
+    partOf = [ "podman-compose-dovecot-root.target" ];
+    wantedBy = [ "podman-compose-dovecot-root.target" ];
+  };
+
+  # Root service
+  # When started, this will automatically create all resources and start
+  # the containers. When stopped, this will teardown all resources.
+  systemd.targets."podman-compose-dovecot-root" = {
+    unitConfig = {
+      Description = "Root target generated by compose2nix.";
+    };
+  };
+}
diff --git a/testdata/TestNoWriteNixSetup.docker.nix b/testdata/TestNoWriteNixSetup.docker.nix
index 3d06da7..a3d1eda 100644
--- a/testdata/TestNoWriteNixSetup.docker.nix
+++ b/testdata/TestNoWriteNixSetup.docker.nix
@@ -63,7 +63,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestNoWriteNixSetup.podman.nix b/testdata/TestNoWriteNixSetup.podman.nix
index f32db8c..d59bb46 100644
--- a/testdata/TestNoWriteNixSetup.podman.nix
+++ b/testdata/TestNoWriteNixSetup.podman.nix
@@ -61,7 +61,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestOverrideSystemdStopTimeout.docker.nix b/testdata/TestOverrideSystemdStopTimeout.docker.nix
index 690ea45..dda0c2b 100644
--- a/testdata/TestOverrideSystemdStopTimeout.docker.nix
+++ b/testdata/TestOverrideSystemdStopTimeout.docker.nix
@@ -70,7 +70,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestOverrideSystemdStopTimeout.podman.nix b/testdata/TestOverrideSystemdStopTimeout.podman.nix
index 854c100..8d4363b 100644
--- a/testdata/TestOverrideSystemdStopTimeout.podman.nix
+++ b/testdata/TestOverrideSystemdStopTimeout.podman.nix
@@ -73,7 +73,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestProject.docker.nix b/testdata/TestProject.docker.nix
index b640833..ff406c1 100644
--- a/testdata/TestProject.docker.nix
+++ b/testdata/TestProject.docker.nix
@@ -69,7 +69,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestProject.podman.nix b/testdata/TestProject.podman.nix
index c0465ed..5f81348 100644
--- a/testdata/TestProject.podman.nix
+++ b/testdata/TestProject.podman.nix
@@ -72,7 +72,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestRemoveVolumes.docker.nix b/testdata/TestRemoveVolumes.docker.nix
index 3c0342d..bbecaa1 100644
--- a/testdata/TestRemoveVolumes.docker.nix
+++ b/testdata/TestRemoveVolumes.docker.nix
@@ -69,7 +69,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestRemoveVolumes.podman.nix b/testdata/TestRemoveVolumes.podman.nix
index 424764a..307a894 100644
--- a/testdata/TestRemoveVolumes.podman.nix
+++ b/testdata/TestRemoveVolumes.podman.nix
@@ -72,7 +72,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestSystemdMount.docker.nix b/testdata/TestSystemdMount.docker.nix
index 4780f3b..5002282 100644
--- a/testdata/TestSystemdMount.docker.nix
+++ b/testdata/TestSystemdMount.docker.nix
@@ -72,7 +72,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestSystemdMount.podman.nix b/testdata/TestSystemdMount.podman.nix
index fde57e3..daa1ee0 100644
--- a/testdata/TestSystemdMount.podman.nix
+++ b/testdata/TestSystemdMount.podman.nix
@@ -75,7 +75,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestUpheldBy.docker.nix b/testdata/TestUpheldBy.docker.nix
index da3a296..ca0a9b8 100644
--- a/testdata/TestUpheldBy.docker.nix
+++ b/testdata/TestUpheldBy.docker.nix
@@ -72,7 +72,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";
diff --git a/testdata/TestUpheldBy.podman.nix b/testdata/TestUpheldBy.podman.nix
index 05c21e1..d0d5da3 100644
--- a/testdata/TestUpheldBy.podman.nix
+++ b/testdata/TestUpheldBy.podman.nix
@@ -75,7 +75,7 @@
       "DOCKER_MODS" = "ghcr.io/gilbn/theme.park:sabnzbd";
       "PGID" = "1000";
       "PUID" = "1000";
-      "TP_DOMAIN" = "hey.hello.us\/themepark";
+      "TP_DOMAIN" = "hey.hello.us\\/themepark";
       "TP_HOTIO" = "false";
       "TP_THEME" = "potato";
       "TZ" = "America/New_York";