feat: implement path transforms
diff --git a/docs/tar.md b/docs/tar.md
index d7f94ba..58d5eb1 100644
--- a/docs/tar.md
+++ b/docs/tar.md
@@ -25,11 +25,31 @@
 ## mtree_spec
 
 <pre>
-mtree_spec(<a href="#mtree_spec-name">name</a>, <a href="#mtree_spec-out">out</a>, <a href="#mtree_spec-srcs">srcs</a>)
+mtree_spec(<a href="#mtree_spec-name">name</a>, <a href="#mtree_spec-out">out</a>, <a href="#mtree_spec-srcs">srcs</a>, <a href="#mtree_spec-transform">transform</a>)
 </pre>
 
 Create an mtree specification to map a directory hierarchy. See https://man.freebsd.org/cgi/man.cgi?mtree(8)
 
+Supports `$` and `^` RegExp tokens, which may be used together.
+
+* for stripping prefix, use `^path/to/strip`
+* for stripping suffix, use `path/to/strip$`
+* for exact match and replace, use `^path/to/strip$`
+* for partial match and replace, use `replace_anywhere`
+
+
+An example of stripping package path relative to the workspace
+
+```starlark
+tar(
+    srcs = ["PKGINFO"],
+    transform = {
+        "^{}".format(package_name()): ""
+    }
+)
+```
+
+
 **ATTRIBUTES**
 
 
@@ -38,6 +58,7 @@
 | <a id="mtree_spec-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/docs/build-ref.html#name">Name</a> | required |  |
 | <a id="mtree_spec-out"></a>out |  Resulting specification file to write   | <a href="https://bazel.build/docs/build-ref.html#labels">Label</a> | optional |  |
 | <a id="mtree_spec-srcs"></a>srcs |  Files that are placed into the tar   | <a href="https://bazel.build/docs/build-ref.html#labels">List of labels</a> | required |  |
+| <a id="mtree_spec-transform"></a>transform |  A dict for path transforming. These are applied serially in respect to their orders.   | <a href="https://bazel.build/docs/skylark/lib/dict.html">Dictionary: String -> String</a> | optional | {} |
 
 
 <a id="tar_rule"></a>
diff --git a/lib/private/tar.bzl b/lib/private/tar.bzl
index 4b301e5..29f0dec 100644
--- a/lib/private/tar.bzl
+++ b/lib/private/tar.bzl
@@ -51,6 +51,7 @@
 
 _mtree_attrs = {
     "srcs": attr.label_list(doc = "Files that are placed into the tar", mandatory = True, allow_files = True),
+    "transform": attr.string_dict(doc = """A dict for path transforming. These are applied serially in respect to their orders."""),
     "out": attr.output(doc = "Resulting specification file to write"),
 }
 
@@ -124,11 +125,7 @@
 
     return DefaultInfo(files = depset([out]), runfiles = ctx.runfiles([out]))
 
-def _default_mtree_line(file):
-    # Functions passed to map_each cannot take optional arguments.
-    return _mtree_line(file.short_path, file.path, "dir" if file.is_directory else "file")
-
-def _mtree_line(file, content, type, uid = "0", gid = "0", time = "1672560000", mode = "0755"):
+def _mtree_line(file, content, type, uid = "0", gid = "0", time = "1672560000.000000", mode = "0755"):
     return " ".join([
         file,
         "uid=" + uid,
@@ -139,12 +136,32 @@
         "content=" + content,
     ])
 
+def _transform(path, transforms):
+    for (match, replace) in transforms.items():
+        # full match
+        if match.startswith("^") and match.endswith("$"):
+            if match.removeprefix("^").removesuffix("$") == path:
+                path = replace
+        elif match.startswith("^"):
+            if path.startswith(match.removeprefix("^")):
+                path = "".join([replace, path.removeprefix(match.removeprefix("^"))])
+        elif match.endswith("$"):
+            if path.endswith(match.removesuffix("$")):
+                path = "".join([path.removesuffix(match.removesuffix("$")), replace])
+        else:
+            path = path.replace(match, replace)
+
+    return path
+
 def _mtree_impl(ctx):
     out = ctx.outputs.out or ctx.actions.declare_file(ctx.attr.name + ".spec")
 
     content = ctx.actions.args()
     content.set_param_file_format("multiline")
-    content.add_all(ctx.files.srcs, map_each = _default_mtree_line)
+
+    for s in ctx.files.srcs:
+        path = _transform(s.short_path, ctx.attr.transform)
+        content.add(_mtree_line(path, s.path, "dir" if s.is_directory else "file"))
 
     for s in ctx.attr.srcs:
         default_info = s[DefaultInfo]
@@ -153,7 +170,7 @@
 
         runfiles_dir = _calculate_runfiles_dir(default_info)
         for file in depset(transitive = [s.default_runfiles.files]).to_list():
-            destination = _runfile_path(ctx, file, runfiles_dir)
+            destination = _transform(_runfile_path(ctx, file, runfiles_dir), ctx.attr.transform)
             content.add(_mtree_line(destination, file.path, "file"))
 
     ctx.actions.write(out, content = content)
diff --git a/lib/tar.bzl b/lib/tar.bzl
index 172c07e..98d3ec6 100644
--- a/lib/tar.bzl
+++ b/lib/tar.bzl
@@ -23,7 +23,27 @@
 load("//lib/private:tar.bzl", "tar_lib", _tar = "tar")
 
 mtree_spec = rule(
-    doc = "Create an mtree specification to map a directory hierarchy. See https://man.freebsd.org/cgi/man.cgi?mtree(8)",
+    doc = """Create an mtree specification to map a directory hierarchy. See https://man.freebsd.org/cgi/man.cgi?mtree(8)
+
+Supports `$` and `^` RegExp tokens, which may be used together.
+
+* for stripping prefix, use `^path/to/strip`
+* for stripping suffix, use `path/to/strip$`
+* for exact match and replace, use `^path/to/strip$`
+* for partial match and replace, use `replace_anywhere`
+
+
+An example of stripping package path relative to the workspace
+
+```starlark
+tar(
+    srcs = ["PKGINFO"],
+    transform = {
+        "^{}".format(package_name()): ""
+    }
+)
+```
+""",
     implementation = tar_lib.mtree_implementation,
     attrs = tar_lib.mtree_attrs,
 )
@@ -69,8 +89,11 @@
             name = mtree_target,
             srcs = kwargs["srcs"],
             out = "{}.txt".format(mtree_target),
+            transform = kwargs.pop("transform", {}),
         )
     elif types.is_list(mtree):
+        if kwargs.pop("transform", None):
+            fail("transform shall be provided only when mtree=auto")
         write_file(
             name = mtree_target,
             out = "{}.txt".format(mtree_target),
diff --git a/lib/tests/tar/BUILD.bazel b/lib/tests/tar/BUILD.bazel
index 2851bae..c781111 100644
--- a/lib/tests/tar/BUILD.bazel
+++ b/lib/tests/tar/BUILD.bazel
@@ -199,3 +199,30 @@
     file1 = "src_file",
     file2 = "cat_src_file_output",
 )
+
+tar(
+    name = "transform",
+    srcs = [
+        "pkg/debian-binary",
+        "src_file",
+    ],
+    out = "transform.tar",
+    transform = {
+        "^lib/tests/": "",
+        "src_file$": "PKGINFO",
+        "^tar": "package",
+        # will flatten pkg/debian-binary to be inside package
+        "pkg/debian-binary": "debian-binary",
+    },
+)
+
+assert_tar_listing(
+    name = "test_transform",
+    actual = "transform",
+    expected = [
+        #
+        # TODO: https://github.com/aspect-build/bazel-lib/issues/625
+        "-rwxr-xr-x  0 0      0           0 Jan  1  2023 package/debian-binary",
+        "-rwxr-xr-x  0 0      0          21 Jan  1  2023 package/PKGINFO",
+    ],
+)
diff --git a/lib/tests/tar/pkg/debian-binary b/lib/tests/tar/pkg/debian-binary
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/tests/tar/pkg/debian-binary