[assembly] Add product-provided config-data to schema

This adds product-provided config_data to the product assembly
configuration schema.

It's only stubbed out in this CL.

Change-Id: I926ab96cb3e085f175835a08c01332f80c8c7b07
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/686764
Reviewed-by: Aidan Wolter <awolter@google.com>
Reviewed-by: Yaar Schnitman <yaar@google.com>
Commit-Queue: Aaron Wood <aaronwood@google.com>
Fuchsia-Auto-Submit: Aaron Wood <aaronwood@google.com>
diff --git a/build/assembly/product_assembly_configuration.gni b/build/assembly/product_assembly_configuration.gni
index 7f9f399..2043a32 100644
--- a/build/assembly/product_assembly_configuration.gni
+++ b/build/assembly/product_assembly_configuration.gni
@@ -126,9 +126,20 @@
         packages.cache = []
       }
 
-      packages.base += rebase_path(files.base_package_manifests, root_build_dir)
-      packages.cache +=
-          rebase_path(files.cache_package_manifests, root_build_dir)
+      foreach(manifest_path, files.base_package_manifests) {
+        packages.base += [
+          {
+            manifest = rebase_path(manifest_path, root_build_dir)
+          },
+        ]
+      }
+      foreach(manifest_path, files.cache_package_manifests) {
+        packages.cache += [
+          {
+            manifest = rebase_path(manifest_path, root_build_dir)
+          },
+        ]
+      }
     }
   }
 
diff --git a/build/assembly/scripts/compare_image_assembly_config_contents.py b/build/assembly/scripts/compare_image_assembly_config_contents.py
index 2962d1f..c246f32 100644
--- a/build/assembly/scripts/compare_image_assembly_config_contents.py
+++ b/build/assembly/scripts/compare_image_assembly_config_contents.py
@@ -205,16 +205,25 @@
     errors = []
     errors.extend(
         compare_pkg_sets(
-            legacy.base | set(product_packages.get('base', [])), generated.base,
-            "base"))
+            legacy.base | set(
+                [
+                    entry["manifest"]
+                    for entry in product_packages.get('base', [])
+                ]), generated.base, "base"))
     errors.extend(
         compare_pkg_sets(
-            legacy.cache | set(product_packages.get('cache', [])),
-            generated.cache, "cache"))
+            legacy.cache | set(
+                [
+                    entry["manifest"]
+                    for entry in product_packages.get('cache', [])
+                ]), generated.cache, "cache"))
     errors.extend(
         compare_pkg_sets(
-            legacy.system | set(product_packages.get('system', [])),
-            generated.system, "system"))
+            legacy.system | set(
+                [
+                    entry["manifest"]
+                    for entry in product_packages.get('system', [])
+                ]), generated.system, "system"))
 
     errors.extend(
         compare_file_entry_sets(
diff --git a/build/assembly/scripts/generated_assembly_inputs.py b/build/assembly/scripts/generated_assembly_inputs.py
index 7940d5c..5fdeab2 100644
--- a/build/assembly/scripts/generated_assembly_inputs.py
+++ b/build/assembly/scripts/generated_assembly_inputs.py
@@ -10,6 +10,8 @@
 
 from depfile import DepFile
 
+from typing import Dict, Optional
+
 
 def main():
     parser = argparse.ArgumentParser(
@@ -50,13 +52,18 @@
     # Add a package and all the included blobs.
     manifests_for_depfile = []
 
-    def add_package(manifest):
+    def add_package(entry: Dict):
+        manifest = entry["manifest"]
         manifests_for_depfile.append(manifest)
         add_source(manifest)
         with open(manifest, 'r') as f:
             manifest = json.load(f)
             for blob in manifest.get("blobs", []):
                 add_source(blob["source_path"])
+        config_data: Optional[Dict[str, str]] = entry.get("config_data")
+        if config_data:
+            for (_dest, source) in config_data.items():
+                add_source(source)
 
     # Add the product config.
     add_source(args.product_config.name)
diff --git a/src/developer/ffx/plugins/assembly/BUILD.gn b/src/developer/ffx/plugins/assembly/BUILD.gn
index 1b482c2..e0f414e 100644
--- a/src/developer/ffx/plugins/assembly/BUILD.gn
+++ b/src/developer/ffx/plugins/assembly/BUILD.gn
@@ -50,6 +50,7 @@
     "//src/lib/assembly/images_manifest",
     "//src/lib/assembly/minfs",
     "//src/lib/assembly/package_list",
+    "//src/lib/assembly/package_utils",
     "//src/lib/assembly/partitions_config",
     "//src/lib/assembly/structured_config",
     "//src/lib/assembly/test_keys",
diff --git a/src/developer/ffx/plugins/assembly/src/operations/product/assembly_builder.rs b/src/developer/ffx/plugins/assembly/src/operations/product/assembly_builder.rs
index e3198a1..5b3f5191 100644
--- a/src/developer/ffx/plugins/assembly/src/operations/product/assembly_builder.rs
+++ b/src/developer/ffx/plugins/assembly/src/operations/product/assembly_builder.rs
@@ -163,10 +163,10 @@
     /// flagged as being the issue (and not the platform being the issue).
     pub fn add_product_packages(&mut self, packages: &ProductPackagesConfig) -> Result<()> {
         for p in &packages.base {
-            self.base.add_package_from_path(p)?
+            self.base.add_package_from_path(p.manifest.as_std_path())?
         }
         for p in &packages.cache {
-            self.cache.add_package_from_path(p)?
+            self.cache.add_package_from_path(p.manifest.as_std_path())?
         }
         Ok(())
     }
@@ -469,22 +469,24 @@
 #[cfg(test)]
 mod tests {
     use super::*;
+    use assembly_package_utils::PackageManifestPathBuf;
     use camino::Utf8PathBuf;
     use fuchsia_pkg::{PackageBuilder, PackageManifest};
     use std::fs::File;
     use tempfile::TempDir;
 
-    fn write_empty_pkg(path: impl AsRef<Path>, name: &str) -> Utf8PathBuf {
+    fn write_empty_pkg(path: impl AsRef<Path>, name: &str) -> PackageManifestPathBuf {
         let path = path.as_ref();
         let mut builder = PackageBuilder::new(name);
         let manifest_path = path.join(name);
         builder.manifest_path(&manifest_path);
         builder.build(path, path.join(format!("{}_meta.far", name))).unwrap();
-        Utf8PathBuf::from_path_buf(manifest_path).unwrap()
+        Utf8PathBuf::from_path_buf(manifest_path).unwrap().into()
     }
 
     fn make_test_assembly_bundle(bundle_path: &Path) -> AssemblyInputBundle {
-        let write_empty_bundle_pkg = |name: &str| write_empty_pkg(bundle_path, name).into();
+        let write_empty_bundle_pkg =
+            |name: &str| write_empty_pkg(bundle_path, name).clone().into_std_path_buf();
         AssemblyInputBundle {
             image_assembly: image_assembly_config::PartialImageAssemblyConfig {
                 base: vec![write_empty_bundle_pkg("base_package0")],
@@ -604,8 +606,14 @@
         let outdir = TempDir::new().unwrap();
 
         let packages = ProductPackagesConfig {
-            base: vec![write_empty_pkg(&outdir, "base_a"), write_empty_pkg(&outdir, "base_b")],
-            cache: vec![write_empty_pkg(&outdir, "cache_a"), write_empty_pkg(&outdir, "cache_b")],
+            base: vec![
+                write_empty_pkg(&outdir, "base_a").into(),
+                write_empty_pkg(&outdir, "base_b").into(),
+            ],
+            cache: vec![
+                write_empty_pkg(&outdir, "cache_a").into(),
+                write_empty_pkg(&outdir, "cache_b").into(),
+            ],
         };
         let minimum_bundle = AssemblyInputBundle {
             image_assembly: image_assembly_config::PartialImageAssemblyConfig {
@@ -647,7 +655,7 @@
         let outdir = TempDir::new().unwrap();
 
         let packages = ProductPackagesConfig {
-            base: vec![write_empty_pkg(&outdir, "base_a")],
+            base: vec![write_empty_pkg(&outdir, "base_a").into()],
             ..ProductPackagesConfig::default()
         };
         let minimum_bundle = AssemblyInputBundle {
diff --git a/src/lib/assembly/BUILD.gn b/src/lib/assembly/BUILD.gn
index a706b66..a102217 100644
--- a/src/lib/assembly/BUILD.gn
+++ b/src/lib/assembly/BUILD.gn
@@ -14,6 +14,7 @@
       "images_config:host_tests",
       "images_manifest:host_tests",
       "minfs:host_tests",
+      "package_utils:host_tests",
       "partitions_config:host_tests",
       "structured_config:host_tests",
       "test_util:host_tests",
diff --git a/src/lib/assembly/config/BUILD.gn b/src/lib/assembly/config/BUILD.gn
index a7cda17..5f7a5a0 100644
--- a/src/lib/assembly/config/BUILD.gn
+++ b/src/lib/assembly/config/BUILD.gn
@@ -14,6 +14,7 @@
       "//sdk/fidl/fuchsia.logger:fuchsia.logger-rustc",
       "//src/developer/ffx/config:lib",
       "//src/lib/assembly/fvm",
+      "//src/lib/assembly/package_utils",
       "//src/lib/assembly/util",
       "//third_party/rust_crates:anyhow",
       "//third_party/rust_crates:assert_matches",
diff --git a/src/lib/assembly/config/src/product_config.rs b/src/lib/assembly/config/src/product_config.rs
index 84f4ee1..89951b6 100644
--- a/src/lib/assembly/config/src/product_config.rs
+++ b/src/lib/assembly/config/src/product_config.rs
@@ -5,6 +5,7 @@
 use crate as image_assembly_config;
 use crate::FileEntry;
 use anyhow::ensure;
+use assembly_package_utils::{PackageInternalPathBuf, PackageManifestPathBuf, SourcePathBuf};
 use camino::Utf8PathBuf;
 use fidl_fuchsia_logger::MAX_TAGS;
 use serde::{Deserialize, Serialize};
@@ -68,15 +69,65 @@
 }
 
 /// Packages provided by the product, to add to the assembled images.
+///
+/// This also includes configuration for those packages:
+///
+/// ```json5
+///   packages: {
+///     base: [
+///       {
+///         manifest: "path/to/package_a/package_manifest.json",
+///       },
+///       {
+///         manifest: "path/to/package_b/package_manifest.json",
+///         config_data: {
+///           "foo.cfg": "path/to/some/source/file/foo.cfg",
+///           "bar/more/data.json": "path/to/some.json",
+///         },
+///       },
+///     ],
+///     cache: []
+///   }
+/// ```
+///
 #[derive(Debug, Default, Deserialize, Serialize)]
 pub struct ProductPackagesConfig {
-    /// Paths to package manifests for packages to add to the 'base' package set.
+    /// Paths to package manifests, or more detailed json entries for packages
+    /// to add to the 'base' package set.
     #[serde(default)]
-    pub base: Vec<Utf8PathBuf>,
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    pub base: Vec<ProductPackageDetails>,
 
-    /// Paths to package manifests for packages to add to the 'cache' package set.
+    /// Paths to package manifests, or more detailed json entries for packages
+    /// to add to the 'cache' package set.
     #[serde(default)]
-    pub cache: Vec<Utf8PathBuf>,
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    pub cache: Vec<ProductPackageDetails>,
+}
+
+/// Describes in more detail a package to add to the assembly.
+#[derive(Debug, PartialEq, Deserialize, Serialize)]
+pub struct ProductPackageDetails {
+    /// Path to the package manifest for this package.
+    pub manifest: PackageManifestPathBuf,
+
+    /// Map of config_data entries for this package, from the destination path
+    /// within the package, to the path where the source file is to be found.
+    #[serde(default)]
+    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
+    pub config_data: BTreeMap<PackageInternalPathBuf, SourcePathBuf>,
+}
+
+impl From<PackageManifestPathBuf> for ProductPackageDetails {
+    fn from(manifest: PackageManifestPathBuf) -> Self {
+        Self { manifest, config_data: BTreeMap::default() }
+    }
+}
+
+impl From<&str> for ProductPackageDetails {
+    fn from(s: &str) -> Self {
+        ProductPackageDetails { manifest: s.into(), config_data: BTreeMap::default() }
+    }
 }
 
 const BASE_CONSOLE_ALLOWED_TAGS: &[&str] = &[
@@ -300,10 +351,10 @@
               product: {
                   packages: {
                       base: [
-                          "path/to/base/package_manifest.json"
+                          { manifest: "path/to/base/package_manifest.json" }
                       ],
                       cache: [
-                          "path/to/cache/package_manifest.json"
+                          { manifest: "path/to/cache/package_manifest.json" }
                       ]
                   }
               },
@@ -313,8 +364,118 @@
         let mut cursor = std::io::Cursor::new(json5);
         let config: ProductAssemblyConfig = util::from_reader(&mut cursor).unwrap();
         assert_eq!(config.platform.build_type, BuildType::Eng);
-        assert_eq!(config.product.packages.base, vec!["path/to/base/package_manifest.json"]);
-        assert_eq!(config.product.packages.cache, vec!["path/to/cache/package_manifest.json"]);
+        assert_eq!(
+            config.product.packages.base,
+            vec![ProductPackageDetails {
+                manifest: "path/to/base/package_manifest.json".into(),
+                config_data: BTreeMap::default()
+            }]
+        );
+        assert_eq!(
+            config.product.packages.cache,
+            vec![ProductPackageDetails {
+                manifest: "path/to/cache/package_manifest.json".into(),
+                config_data: BTreeMap::default()
+            }]
+        );
+    }
+
+    #[test]
+    fn test_product_provided_config_data() {
+        let json5 = r#"
+            {
+                base: [
+                    {
+                        manifest: "path/to/base/package_manifest.json"
+                    },
+                    {
+                        manifest: "some/other/manifest.json",
+                        config_data: {
+                            "dest/path/cfg.txt": "source/path/cfg.txt",
+                            "other_data.json": "source_other_data.json",
+                        }
+                    }
+                  ],
+                cache: [
+                    {
+                        manifest: "path/to/cache/package_manifest.json"
+                    }
+                ]
+            }
+        "#;
+
+        let mut cursor = std::io::Cursor::new(json5);
+        let packages: ProductPackagesConfig = util::from_reader(&mut cursor).unwrap();
+        assert_eq!(
+            packages.base,
+            vec![
+                ProductPackageDetails::from("path/to/base/package_manifest.json"),
+                ProductPackageDetails {
+                    manifest: "some/other/manifest.json".into(),
+                    config_data: BTreeMap::from([
+                        ("dest/path/cfg.txt".into(), "source/path/cfg.txt".into()),
+                        ("other_data.json".into(), "source_other_data.json".into())
+                    ])
+                }
+            ]
+        );
+        assert_eq!(packages.cache, vec!["path/to/cache/package_manifest.json".into()]);
+    }
+
+    #[test]
+    fn product_package_details_deserialization() {
+        let json5 = r#"
+            {
+                manifest: "some/other/manifest.json",
+                config_data: {
+                    "dest/path/cfg.txt": "source/path/cfg.txt",
+                    "other_data.json": "source_other_data.json",
+                }
+            }
+        "#;
+        let expected = ProductPackageDetails {
+            manifest: "some/other/manifest.json".into(),
+            config_data: BTreeMap::from([
+                ("dest/path/cfg.txt".into(), "source/path/cfg.txt".into()),
+                ("other_data.json".into(), "source_other_data.json".into()),
+            ]),
+        };
+        let mut cursor = std::io::Cursor::new(json5);
+        let details: ProductPackageDetails = util::from_reader(&mut cursor).unwrap();
+        assert_eq!(details, expected);
+    }
+
+    #[test]
+    fn product_package_details_serialization() {
+        let entries = vec![
+            ProductPackageDetails {
+                manifest: "path/to/manifest.json".into(),
+                config_data: BTreeMap::default(),
+            },
+            ProductPackageDetails {
+                manifest: "another/path/to/a/manifest.json".into(),
+                config_data: BTreeMap::from([
+                    ("dest/path/A".into(), "source/path/A".into()),
+                    ("dest/path/B".into(), "source/path/B".into()),
+                ]),
+            },
+        ];
+        let serialized = serde_json::to_value(&entries).unwrap();
+        let expected = serde_json::json!(
+            [
+                {
+                    "manifest": "path/to/manifest.json"
+                },
+                {
+                    "manifest": "another/path/to/a/manifest.json",
+                    "config_data": {
+                        "dest/path/A": "source/path/A",
+                        "dest/path/B": "source/path/B"
+                    }
+                }
+            ]
+        );
+        assert_eq!(serialized, expected);
     }
 
     #[test]
diff --git a/src/lib/assembly/package_utils/BUILD.gn b/src/lib/assembly/package_utils/BUILD.gn
new file mode 100644
index 0000000..db9267e
--- /dev/null
+++ b/src/lib/assembly/package_utils/BUILD.gn
@@ -0,0 +1,30 @@
+# Copyright 2022 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/rust/rustc_library.gni")
+
+rustc_library("package_utils") {
+  edition = "2021"
+  name = "assembly_package_utils"
+  version = "0.1.0"
+  with_unit_tests = true
+  deps = [
+    "//src/lib/assembly/util",
+    "//third_party/rust_crates:anyhow",
+    "//third_party/rust_crates:serde",
+  ]
+  test_deps = [
+    "//third_party/rust_crates:serde_json",
+    "//third_party/rust_crates:serde_json5",
+  ]
+  sources = [
+    "src/lib.rs",
+    "src/package_utils.rs",
+  ]
+}
+
+group("host_tests") {
+  testonly = true
+  deps = [ ":package_utils_test" ]
+}
diff --git a/src/lib/assembly/package_utils/src/lib.rs b/src/lib/assembly/package_utils/src/lib.rs
new file mode 100644
index 0000000..cb291c7
--- /dev/null
+++ b/src/lib/assembly/package_utils/src/lib.rs
@@ -0,0 +1,9 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// 'package_utils' is a crate of utility types and fns for working with
+/// fuchsia-packages.
+pub use package_utils::{PackageInternalPathBuf, PackageManifestPathBuf, SourcePathBuf};
+
+mod package_utils;
diff --git a/src/lib/assembly/package_utils/src/package_utils.rs b/src/lib/assembly/package_utils/src/package_utils.rs
new file mode 100644
index 0000000..ce905bd9
--- /dev/null
+++ b/src/lib/assembly/package_utils/src/package_utils.rs
@@ -0,0 +1,43 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use assembly_util::{impl_path_type_marker, PathTypeMarker, TypedPathBuf};
+use serde::{Deserialize, Serialize};
+
+/// PackageIdentity is an opaque type that allows for the string that's used as
+/// a package's identity to be evolved over time, compared with other instances,
+/// and used as a key in maps / sets.
+#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
+struct PackageIdentity(String);
+
+impl std::str::FromStr for PackageIdentity {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(Self(s.to_owned()))
+    }
+}
+
+/// The marker trait for paths within a package
+pub struct InternalPathMarker {}
+impl_path_type_marker!(InternalPathMarker);
+
+/// The semantic type for paths within a package
+pub type PackageInternalPathBuf = TypedPathBuf<InternalPathMarker>;
+
+/// The marker trait for the source path when that's ambiguous (like in a list
+/// of source to destination paths)
+pub struct SourcePathMarker {}
+impl_path_type_marker!(SourcePathMarker);
+
+/// The semantic type for paths that are the path to the source of a file to use
+/// in some context.  Such as the source file for a blob in a package.
+pub type SourcePathBuf = TypedPathBuf<SourcePathMarker>;
+
+/// The marker trait for paths to a PackageManifest
+pub struct PackageManifestPathMarker {}
+impl_path_type_marker!(PackageManifestPathMarker);
+
+/// The semantic type for paths that are the path to a package manifest.
+pub type PackageManifestPathBuf = TypedPathBuf<PackageManifestPathMarker>;
diff --git a/src/lib/assembly/util/src/lib.rs b/src/lib/assembly/util/src/lib.rs
index 8bbe51c..7f58baf7 100644
--- a/src/lib/assembly/util/src/lib.rs
+++ b/src/lib/assembly/util/src/lib.rs
@@ -8,13 +8,14 @@
 
 mod insert_unique;
 mod path_to_string;
+
 mod paths;
 
 pub use insert_unique::{DuplicateKeyError, InsertAllUniqueExt, InsertUniqueExt, MapEntry};
 pub use path_to_string::PathToStringExt;
 pub use paths::{
     normalize_path, path_relative_from, path_relative_from_file, resolve_path,
-    resolve_path_from_file,
+    resolve_path_from_file, PathTypeMarker, TypedPathBuf,
 };
 
 use anyhow::{Context as _, Result};
diff --git a/src/lib/assembly/util/src/paths.rs b/src/lib/assembly/util/src/paths.rs
index 4936eb5..8270175 100644
--- a/src/lib/assembly/util/src/paths.rs
+++ b/src/lib/assembly/util/src/paths.rs
@@ -3,8 +3,189 @@
 // found in the LICENSE file.
 
 use anyhow::{anyhow, Context, Result};
+use camino::Utf8PathBuf;
 use pathdiff::diff_paths;
-use std::path::{Component, Path, PathBuf};
+use serde::{Deserialize, Serialize};
+use std::{
+    hash::Hash,
+    marker::PhantomData,
+    path::{Component, Path, PathBuf},
+};
+
+/// A base trait for TypePath's marker traits.
+pub trait PathTypeMarker {
+    /// A reference to an object that implements Display, and gives the
+    /// displayable semantic type for this path.  This is used by the Debug
+    /// implementation of `TypedPathBuf` to display the semantic type for the
+    /// path:
+    ///
+    /// ```
+    /// struct MarkerStructType;
+    /// impl_path_type_marker!(MarkerStructType);
+    ///
+    /// let typed_path = TypedPathBuf<MarkerStructType>::from("some/path");
+    /// println!("{:?}", typed_path);
+    /// ```
+    /// will print:
+    ///
+    /// ```text
+    /// TypedPathBuf<MarkerStructType>("some/path")
+    /// ```
+    fn path_type_display() -> &'static dyn std::fmt::Display;
+}
+
+/// Implement the `PathTypeMarker` trait for a given marker-type struct.  This
+/// mainly simplifies the creation of a display-string for the type.
+#[macro_export]
+macro_rules! impl_path_type_marker {
+    // This macro takes an argument of the marker struct's type name, and then
+    // provides an implementation of 'PathTypeMarker' for it.
+    ($struct_name:ident) => {
+        impl PathTypeMarker for $struct_name {
+            fn path_type_display() -> &'static dyn std::fmt::Display {
+                &stringify!($struct_name)
+            }
+        }
+    };
+}
+
+/// A path, in valid utf-8, which carries a marker for what kind of path it is.
+#[derive(Clone, Serialize, Deserialize)]
+#[repr(transparent)]
+#[serde(transparent)]
+pub struct TypedPathBuf<P: PathTypeMarker> {
+    #[serde(flatten)]
+    inner: Utf8PathBuf,
+
+    #[serde(skip)]
+    _marker: PhantomData<P>,
+}
+
+/// This derefs into the typed version of utf8 path, not utf8 path itself, so
+/// that it is easier to use in typed contexts, and makes the switchover to
+/// a non-typed context more explicit.
+///
+/// This also causes any path manipulations (join, etc.) to be done without the
+/// semantic type, so that the caller has to be explicit that it's still the
+/// semantic type (using 'into()', for instance).
+impl<P: PathTypeMarker> std::ops::Deref for TypedPathBuf<P> {
+    type Target = Utf8PathBuf;
+
+    fn deref(&self) -> &Self::Target {
+        &self.inner
+    }
+}
+
+impl<P: PathTypeMarker> TypedPathBuf<P> {
+    /// Convert this TypedPathBuf into a standard (OsStr-based) `PathBuf`.  This
+    /// both strips it of semantic type and that it's known to be Utf-8.
+    pub fn into_std_path_buf(self) -> PathBuf {
+        self.inner.into_std_path_buf()
+    }
+}
+
+/// The Debug implementation displays like a type-struct that carries the marker
+/// type for the path:
+///
+/// ```text
+/// TypedPathBuf<MarkerStructType>("some/path")
+/// ```
+impl<P: PathTypeMarker> std::fmt::Debug for TypedPathBuf<P> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_tuple(&format!("TypedPathBuf<{}>", P::path_type_display()))
+            .field(&self.inner.to_string())
+            .finish()
+    }
+}
+
+/// The Display implementation defers to the wrapped path.
+impl<P: PathTypeMarker> std::fmt::Display for TypedPathBuf<P> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.inner.fmt(f)
+    }
+}
+
+/// Implement From<> for path-like sources.  Note that these also will infer the
+/// semantic type, which while useful in some contexts, can cause issues in
+/// places where multiple different type markers are used:
+///
+/// ```
+/// fn some_func(source: TypedPathBuf<Source>, TypedPathBuf<Destination>);
+///
+/// // This infers the types of the paths:
+/// some_func("source_path".into(), "destination_path".into());
+///
+/// // allowing this error:
+/// some_func("destination_path".into(), "source_path",into());
+///
+/// // In these cases, it's best to strongly type one or both of them:
+/// some_func(TypedPathBuf<Source>::from("source_path"), "destination_path".into());
+///
+/// // or (better)
+/// some_func(TypedPathBuf<Source>::from("source_path"),
+///           TypedPathBuf<Destination>::from("destination_path"));
+/// ```
+// inner module used to group impls and to add above documentation.
+mod from_impls {
+    use super::*;
+
+    impl<P: PathTypeMarker> From<Utf8PathBuf> for TypedPathBuf<P> {
+        fn from(path: Utf8PathBuf) -> Self {
+            Self { inner: path, _marker: PhantomData }
+        }
+    }
+
+    impl<P: PathTypeMarker> From<String> for TypedPathBuf<P> {
+        fn from(s: String) -> TypedPathBuf<P> {
+            TypedPathBuf::from(Utf8PathBuf::from(s))
+        }
+    }
+
+    impl<P: PathTypeMarker> From<&str> for TypedPathBuf<P> {
+        fn from(s: &str) -> TypedPathBuf<P> {
+            TypedPathBuf::from(Utf8PathBuf::from(s))
+        }
+    }
+
+    impl<P: PathTypeMarker> std::str::FromStr for TypedPathBuf<P> {
+        type Err = String;
+
+        fn from_str(s: &str) -> Result<Self, Self::Err> {
+            Ok(Self::from(s))
+        }
+    }
+}
+
+// These comparison implementations are required because #[derive(...)] will not
+// derive these if `P` doesn't implement them, but `P` has no reason to
+// implement them, so these implementations just pass through to the Utf8PathBuf
+// implementations.
+
+impl<P: PathTypeMarker> PartialOrd for TypedPathBuf<P> {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        self.inner.partial_cmp(&other.inner)
+    }
+}
+
+impl<P: PathTypeMarker> Ord for TypedPathBuf<P> {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.inner.cmp(&other.inner)
+    }
+}
+
+impl<P: PathTypeMarker> PartialEq for TypedPathBuf<P> {
+    fn eq(&self, other: &Self) -> bool {
+        self.inner == other.inner
+    }
+}
+
+impl<P: PathTypeMarker> Eq for TypedPathBuf<P> {}
+
+impl<P: PathTypeMarker> Hash for TypedPathBuf<P> {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.inner.hash(state);
+    }
+}
 
 /// Helper to make one path relative to a directory.
 ///
@@ -165,8 +346,72 @@
 #[cfg(test)]
 mod tests {
     use super::*;
+    use std::str::FromStr;
     use std::{iter::FromIterator, path::PathBuf};
 
+    struct TestPathType {}
+    impl_path_type_marker!(TestPathType);
+
+    #[test]
+    fn make_typed_path_from_string() {
+        let original: String = "/this/is/a/string".to_string();
+        let typed = TypedPathBuf::<TestPathType>::from_str(&original).unwrap();
+        assert_eq!(typed.to_string(), original);
+    }
+
+    #[test]
+    fn make_typed_path_from_str() {
+        let original: &str = "/this/is/a/string";
+        let typed = TypedPathBuf::<TestPathType>::from_str(&original).unwrap();
+        assert_eq!(typed.to_string(), original);
+    }
+
+    #[test]
+    fn path_type_deserialization() {
+        #[derive(Debug, Deserialize)]
+        struct Sample {
+            pub path: TypedPathBuf<TestPathType>,
+        }
+        let parsed: Sample = serde_json::from_str("{ \"path\": \"this/is/a/path\"}").unwrap();
+        assert_eq!(parsed.path, TypedPathBuf::<TestPathType>::from("this/is/a/path"));
+    }
+
+    #[test]
+    fn path_type_serialization() {
+        #[derive(Debug, Serialize)]
+        struct Sample {
+            pub path: TypedPathBuf<TestPathType>,
+        }
+        let sample = Sample { path: "this/is/a/path".into() };
+        let expected = serde_json::json!({ "path": "this/is/a/path"});
+        assert_eq!(serde_json::to_value(sample).unwrap(), expected);
+    }
+
+    #[test]
+    fn typed_path_debug_impl() {
+        let typed = TypedPathBuf::<TestPathType>::from("some/path");
+        assert_eq!(format!("{:?}", typed), "TypedPathBuf<TestPathType>(\"some/path\")");
+    }
+
+    #[test]
+    fn typed_path_display_impl() {
+        let typed = TypedPathBuf::<TestPathType>::from("some/path");
+        assert_eq!(format!("{}", typed), "some/path");
+    }
+
+    #[test]
+    fn typed_path_buf_into_path_buf() {
+        let typed = TypedPathBuf::<TestPathType>::from("some/path");
+        assert_eq!(typed.into_std_path_buf(), PathBuf::from("some/path"));
+    }
+
+    #[test]
+    fn typed_path_derefs_into_utf8_path() {
+        let typed = TypedPathBuf::<TestPathType>::from("some/path");
+        let utf8_path = Utf8PathBuf::from("some/path");
+        assert_eq!(*typed, utf8_path);
+    }
+
     #[test]
     fn resolve_path_from_file_simple() {
         let result = resolve_path_from_file("an/internal/path", "path/to/manifest.txt").unwrap();