Repository: buildpacks/lifecycle
Pull request #1236 (2023-10-31 19:52:44)

Consolidate methods that read and write platform spec'd TOML
in platform/files package.

With the exception of reading <layers>/config/metadata.toml in the launcher, as we want to avoid importing the buildpack package, and platform/files depends on it.

<!-- 🎉🎉🎉 Thank you for the PR!!! 🎉🎉🎉 -->

### Summary
<!-- Please describe your changes at a high level. -->



#### Release notes
<!-- Please provide 1-2 sentences for release notes. -->
<!-- Example: When using platform API `0.7` or greater, the `creator` logs the expected phase header for the analyze phase -->

Consolidates methods that read and write platform spec'd TOML in the platform/files package



Patch:
diff --git a/buildpack/descriptor.go b/buildpack/descriptor.go
index 0ea29701..22779dbf 100644
--- a/buildpack/descriptor.go
+++ b/buildpack/descriptor.go
@@ -8,7 +8,7 @@ const (
 // Descriptor exposes information contained in a buildpack.toml or extension.toml
 // that is generic to buildpacks and/or image extensions.
 //
-//go:generate mockgen -package testmock -destination ../lifecycle/testmock/component_descriptor.go github.com/buildpacks/lifecycle/buildpack Descriptor
+//go:generate mockgen -package testmock -destination ../phase/testmock/component_descriptor.go github.com/buildpacks/lifecycle/buildpack Descriptor
 type Descriptor interface {
 	API() string
 	Homepage() string
diff --git a/cache/image_comparer.go b/cache/image_comparer.go
index 5faa2a33..f3da61ed 100644
--- a/cache/image_comparer.go
+++ b/cache/image_comparer.go
@@ -5,7 +5,7 @@ import (
 	"github.com/pkg/errors"
 )
 
-//go:generate mockgen -package testmockcache -destination ../lifecycle/testmock/cache/image_comparer.go github.com/buildpacks/lifecycle/cache ImageComparer
+//go:generate mockgen -package testmockcache -destination ../phase/testmock/cache/image_comparer.go github.com/buildpacks/lifecycle/cache ImageComparer
 
 // ImageComparer provides a way to compare images
 type ImageComparer interface {
diff --git a/cmd/lifecycle/analyzer.go b/cmd/lifecycle/analyzer.go
index 0bbdbec2..1421626e 100644
--- a/cmd/lifecycle/analyzer.go
+++ b/cmd/lifecycle/analyzer.go
@@ -12,6 +12,7 @@ import (
 	"github.com/buildpacks/lifecycle/image"
 	"github.com/buildpacks/lifecycle/phase"
 	"github.com/buildpacks/lifecycle/platform"
+	"github.com/buildpacks/lifecycle/platform/files"
 	"github.com/buildpacks/lifecycle/priv"
 )
 
@@ -99,7 +100,7 @@ func (a *analyzeCmd) Exec() error {
 		a.PlatformAPI,
 		&cmd.BuildpackAPIVerifier{},
 		NewCacheHandler(a.keychain),
-		phase.Config,
+		files.Handler,
 		image.NewHandler(a.docker, a.keychain, a.LayoutDir, a.UseLayout, a.InsecureRegistries),
 		image.NewRegistryHandler(a.keychain, a.InsecureRegistries),
 	)
@@ -111,5 +112,5 @@ func (a *analyzeCmd) Exec() error {
 	if err != nil {
 		return cmd.FailErrCode(err, a.CodeFor(platform.AnalyzeError), "analyze")
 	}
-	return phase.Config.WriteAnalyzed(a.AnalyzedPath, &analyzedMD, cmd.DefaultLogger)
+	return files.Handler.WriteAnalyzed(a.AnalyzedPath, &analyzedMD, cmd.DefaultLogger)
 }
diff --git a/cmd/lifecycle/builder.go b/cmd/lifecycle/builder.go
index 50b78eaa..890cb246 100644
--- a/cmd/lifecycle/builder.go
+++ b/cmd/lifecycle/builder.go
@@ -3,12 +3,9 @@ package main
 import (
 	"errors"
 
-	"github.com/BurntSushi/toml"
-
 	"github.com/buildpacks/lifecycle/buildpack"
 	"github.com/buildpacks/lifecycle/cmd"
 	"github.com/buildpacks/lifecycle/cmd/lifecycle/cli"
-	"github.com/buildpacks/lifecycle/internal/encoding"
 	"github.com/buildpacks/lifecycle/launch"
 	"github.com/buildpacks/lifecycle/phase"
 	"github.com/buildpacks/lifecycle/platform"
@@ -66,7 +63,7 @@ func (b *buildCmd) Exec() error {
 	if err = verifyBuildpackApis(group); err != nil {
 		return err
 	}
-	amd, err := files.ReadAnalyzed(b.AnalyzedPath, cmd.DefaultLogger)
+	amd, err := files.Handler.ReadAnalyzed(b.AnalyzedPath, cmd.DefaultLogger)
 	if err != nil {
 		return unwrapErrorFailWithMessage(err, "reading analyzed.toml")
 	}
@@ -93,10 +90,7 @@ func (b *buildCmd) build(group buildpack.Group, plan files.Plan, analyzedMD file
 	if err != nil {
 		return b.unwrapBuildFail(err)
 	}
-	if err = encoding.WriteTOML(launch.GetMetadataFilePath(b.LayersDir), md); err != nil {
-		return cmd.FailErr(err, "write build metadata")
-	}
-	return nil
+	return files.Handler.WriteBuildMetadata(launch.GetMetadataFilePath(b.LayersDir), md)
 }
 
 func (b *buildCmd) unwrapBuildFail(err error) error {
@@ -109,14 +103,13 @@ func (b *buildCmd) unwrapBuildFail(err error) error {
 }
 
 func (b *buildCmd) readData() (buildpack.Group, files.Plan, error) {
-	group, err := phase.ReadGroup(b.GroupPath)
+	group, err := files.Handler.ReadGroup(b.GroupPath)
 	if err != nil {
-		return buildpack.Group{}, files.Plan{}, cmd.FailErr(err, "read buildpack group")
+		return buildpack.Group{}, files.Plan{}, err
 	}
-
-	var plan files.Plan
-	if _, err := toml.DecodeFile(b.PlanPath, &plan); err != nil {
-		return buildpack.Group{}, files.Plan{}, cmd.FailErr(err, "parse detect plan")
+	plan, err := files.Handler.ReadPlan(b.PlanPath)
+	if err != nil {
+		return buildpack.Group{}, files.Plan{}, err
 	}
 	return group, plan, nil
 }
diff --git a/cmd/lifecycle/creator.go b/cmd/lifecycle/creator.go
index c7e16a31..fda90792 100644
--- a/cmd/lifecycle/creator.go
+++ b/cmd/lifecycle/creator.go
@@ -127,7 +127,7 @@ func (c *createCmd) Exec() error {
 		c.PlatformAPI,
 		&cmd.BuildpackAPIVerifier{},
 		NewCacheHandler(c.keychain),
-		phase.NewConfigHandler(),
+		files.NewHandler(),
 		image.NewHandler(c.docker, c.keychain, c.LayoutDir, c.UseLayout, c.InsecureRegistries),
 		image.NewRegistryHandler(c.keychain, c.InsecureRegistries),
 	)
@@ -139,7 +139,7 @@ func (c *createCmd) Exec() error {
 	if err != nil {
 		return err
 	}
-	if err := phase.Config.WriteAnalyzed(c.AnalyzedPath, &analyzedMD, cmd.DefaultLogger); err != nil {
+	if err := files.Handler.WriteAnalyzed(c.AnalyzedPath, &analyzedMD, cmd.DefaultLogger); err != nil {
 		return err
 	}
 
@@ -148,7 +148,7 @@ func (c *createCmd) Exec() error {
 	detectorFactory := phase.NewHermeticFactory(
 		c.PlatformAPI,
 		&cmd.BuildpackAPIVerifier{},
-		phase.NewConfigHandler(),
+		files.NewHandler(),
 		dirStore,
 	)
 	detector, err := detectorFactory.NewDetector(c.Inputs(), cmd.DefaultLogger)
diff --git a/cmd/lifecycle/detector.go b/cmd/lifecycle/detector.go
index de6c2dde..458010a8 100644
--- a/cmd/lifecycle/detector.go
+++ b/cmd/lifecycle/detector.go
@@ -62,7 +62,7 @@ func (d *detectCmd) Exec() error {
 	detectorFactory := phase.NewHermeticFactory(
 		d.PlatformAPI,
 		&cmd.BuildpackAPIVerifier{},
-		phase.NewConfigHandler(),
+		files.NewHandler(),
 		dirStore,
 	)
 	detector, err := detectorFactory.NewDetector(
@@ -85,7 +85,7 @@ func (d *detectCmd) Exec() error {
 		generatorFactory := phase.NewHermeticFactory(
 			d.PlatformAPI,
 			&cmd.BuildpackAPIVerifier{},
-			phase.Config,
+			files.Handler,
 			dirStore,
 		)
 		var generator *phase.Generator
@@ -102,10 +102,10 @@ func (d *detectCmd) Exec() error {
 		if err != nil {
 			return d.unwrapGenerateFail(err)
 		}
-		if err := phase.Config.WriteAnalyzed(d.AnalyzedPath, &result.AnalyzedMD, cmd.DefaultLogger); err != nil {
+		if err := files.Handler.WriteAnalyzed(d.AnalyzedPath, &result.AnalyzedMD, cmd.DefaultLogger); err != nil {
 			return err
 		}
-		if err := phase.Config.WritePlan(d.PlanPath, &result.Plan); err != nil {
+		if err := files.Handler.WritePlan(d.PlanPath, &result.Plan); err != nil {
 			return err
 		}
 	}
@@ -149,10 +149,10 @@ func doDetect(detector *phase.Detector, p *platform.Platform) (buildpack.Group,
 			return buildpack.Group{}, files.Plan{}, cmd.FailErrCode(err, p.CodeFor(platform.DetectError), "detect")
 		}
 	}
-	if err := phase.Config.WriteGroup(p.GroupPath, &group); err != nil {
+	if err := files.Handler.WriteGroup(p.GroupPath, &group); err != nil {
 		return buildpack.Group{}, files.Plan{}, err
 	}
-	if err := phase.Config.WritePlan(p.PlanPath, &plan); err != nil {
+	if err := files.Handler.WritePlan(p.PlanPath, &plan); err != nil {
 		return buildpack.Group{}, files.Plan{}, err
 	}
 	return group, plan, nil
diff --git a/cmd/lifecycle/exporter.go b/cmd/lifecycle/exporter.go
index dd1b73aa..396df08c 100644
--- a/cmd/lifecycle/exporter.go
+++ b/cmd/lifecycle/exporter.go
@@ -7,7 +7,6 @@ import (
 	"strconv"
 	"time"
 
-	"github.com/BurntSushi/toml"
 	"github.com/buildpacks/imgutil"
 	"github.com/buildpacks/imgutil/layout"
 	"github.com/buildpacks/imgutil/local"
@@ -26,7 +25,6 @@ import (
 	"github.com/buildpacks/lifecycle/cmd"
 	"github.com/buildpacks/lifecycle/cmd/lifecycle/cli"
 	"github.com/buildpacks/lifecycle/image"
-	"github.com/buildpacks/lifecycle/internal/encoding"
 	"github.com/buildpacks/lifecycle/layers"
 	"github.com/buildpacks/lifecycle/phase"
 	"github.com/buildpacks/lifecycle/platform"
@@ -96,7 +94,7 @@ func (e *exportCmd) Args(nargs int, args []string) error {
 	}
 	// read analyzed metadata for use in later stages
 	var err error
-	e.persistedData.analyzedMD, err = files.ReadAnalyzed(e.AnalyzedPath, cmd.DefaultLogger)
+	e.persistedData.analyzedMD, err = files.Handler.ReadAnalyzed(e.AnalyzedPath, cmd.DefaultLogger)
 	if err != nil {
 		return err
 	}
@@ -131,9 +129,9 @@ func (e *exportCmd) Privileges() error {
 }
 
 func (e *exportCmd) Exec() error {
-	group, err := phase.ReadGroup(e.GroupPath)
+	group, err := files.Handler.ReadGroup(e.GroupPath)
 	if err != nil {
-		return cmd.FailErr(err, "read buildpack group")
+		return err
 	}
 	if err = verifyBuildpackApis(group); err != nil {
 		return err
@@ -162,19 +160,14 @@ func (e *exportCmd) registryImages() []string {
 
 func (e *exportCmd) export(group buildpack.Group, cacheStore phase.Cache, analyzedMD files.Analyzed) error {
 	artifactsDir, err := os.MkdirTemp("", "lifecycle.exporter.layer")
-
 	if err != nil {
 		return cmd.FailErr(err, "create temp directory")
 	}
 	defer os.RemoveAll(artifactsDir)
 
-	var projectMD files.ProjectMetadata
-	_, err = toml.DecodeFile(e.ProjectMetadataPath, &projectMD)
+	projectMD, err := files.Handler.ReadProjectMetadata(e.ProjectMetadataPath, cmd.DefaultLogger)
 	if err != nil {
-		if !os.IsNotExist(err) {
-			return err
-		}
-		cmd.DefaultLogger.Debugf("no project metadata found at path '%s', project metadata will not be exported\n", e.ProjectMetadataPath)
+		return err
 	}
 
 	exporter := &phase.Exporter{
@@ -226,7 +219,7 @@ func (e *exportCmd) export(group buildpack.Group, cacheStore phase.Cache, analyz
 	if err != nil {
 		return cmd.FailErrCode(err, e.CodeFor(platform.ExportError), "export")
 	}
-	if err = encoding.WriteTOML(e.ReportPath, &report); err != nil {
+	if err = files.Handler.WriteReport(e.ReportPath, &report); err != nil {
 		return cmd.FailErrCode(err, e.CodeFor(platform.ExportError), "write export report")
 	}
 
diff --git a/cmd/lifecycle/extender.go b/cmd/lifecycle/extender.go
index 595ae1f8..4e8e2b58 100644
--- a/cmd/lifecycle/extender.go
+++ b/cmd/lifecycle/extender.go
@@ -10,6 +10,7 @@ import (
 	"github.com/buildpacks/lifecycle/internal/extend/kaniko"
 	"github.com/buildpacks/lifecycle/phase"
 	"github.com/buildpacks/lifecycle/platform"
+	"github.com/buildpacks/lifecycle/platform/files"
 	"github.com/buildpacks/lifecycle/priv"
 )
 
@@ -52,7 +53,7 @@ func (e *extendCmd) Privileges() error {
 }
 
 func (e *extendCmd) Exec() error {
-	extenderFactory := phase.NewHermeticFactory(e.PlatformAPI, &cmd.BuildpackAPIVerifier{}, phase.NewConfigHandler(), platform.NewDirStore("", ""))
+	extenderFactory := phase.NewHermeticFactory(e.PlatformAPI, &cmd.BuildpackAPIVerifier{}, files.NewHandler(), platform.NewDirStore("", ""))
 	applier, err := kaniko.NewDockerfileApplier()
 	if err != nil {
 		return err
diff --git a/cmd/lifecycle/rebaser.go b/cmd/lifecycle/rebaser.go
index c687be4e..e40944a5 100644
--- a/cmd/lifecycle/rebaser.go
+++ b/cmd/lifecycle/rebaser.go
@@ -15,7 +15,6 @@ import (
 	"github.com/buildpacks/lifecycle/cmd"
 	"github.com/buildpacks/lifecycle/cmd/lifecycle/cli"
 	"github.com/buildpacks/lifecycle/image"
-	"github.com/buildpacks/lifecycle/internal/encoding"
 	"github.com/buildpacks/lifecycle/phase"
 	"github.com/buildpacks/lifecycle/platform"
 	"github.com/buildpacks/lifecycle/platform/files"
@@ -137,8 +136,7 @@ func (r *rebaseCmd) Exec() error {
 	if err != nil {
 		return cmd.FailErrCode(err, r.CodeFor(platform.RebaseError), "rebase")
 	}
-
-	if err := encoding.WriteTOML(r.ReportPath, &report); err != nil {
+	if err = files.Handler.WriteRebaseReport(r.ReportPath, &report); err != nil {
 		return cmd.FailErrCode(err, r.CodeFor(platform.RebaseError), "write rebase report")
 	}
 	return nil
diff --git a/cmd/lifecycle/restorer.go b/cmd/lifecycle/restorer.go
index be1d2ed2..0fba2672 100644
--- a/cmd/lifecycle/restorer.go
+++ b/cmd/lifecycle/restorer.go
@@ -96,17 +96,16 @@ func (r *restoreCmd) Privileges() error {
 }
 
 func (r *restoreCmd) Exec() error {
-	group, groupExt, err := phase.Config.ReadGroup(r.GroupPath)
+	group, err := files.Handler.ReadGroup(r.GroupPath)
 	if err != nil {
-		return cmd.FailErr(err, "read buildpack group")
+		return err
 	}
-	if err := verifyBuildpackApis(buildpack.Group{Group: group}); err != nil {
+	if err = verifyBuildpackApis(group); err != nil {
 		return err
 	}
-	groupFile := buildpack.Group{Group: group, GroupExtensions: groupExt}
 
 	var analyzedMD files.Analyzed
-	if analyzedMD, err = files.ReadAnalyzed(r.AnalyzedPath, cmd.DefaultLogger); err == nil {
+	if analyzedMD, err = files.Handler.ReadAnalyzed(r.AnalyzedPath, cmd.DefaultLogger); err == nil {
 		if r.supportsBuildImageExtension() && r.BuildImageRef != "" {
 			cmd.DefaultLogger.Debugf("Pulling builder image metadata for %s...", r.BuildImageRef)
 			remoteBuildImage, err := r.pullSparse(r.BuildImageRef)
@@ -137,7 +136,7 @@ func (r *restoreCmd) Exec() error {
 			if err = r.updateAnalyzedMD(&analyzedMD, runImage); err != nil {
 				return cmd.FailErr(err, "update analyzed metadata")
 			}
-		} else if r.needsUpdating(analyzedMD.RunImage, groupFile) {
+		} else if r.needsUpdating(analyzedMD.RunImage, group) {
 			cmd.DefaultLogger.Debugf("Updating run image info in analyzed metadata...")
 			h := image.NewHandler(r.docker, r.keychain, r.LayoutDir, r.UseLayout, r.InsecureRegistries)
 			runImage, err = h.InitImage(runImageName)
@@ -148,7 +147,7 @@ func (r *restoreCmd) Exec() error {
 				return cmd.FailErr(err, "update analyzed metadata")
 			}
 		}
-		if err = encoding.WriteTOML(r.AnalyzedPath, analyzedMD); err != nil {
+		if err = files.Handler.WriteAnalyzed(r.AnalyzedPath, &analyzedMD, cmd.DefaultLogger); err != nil {
 			return cmd.FailErr(err, "write analyzed metadata")
 		}
 	} else {
@@ -159,7 +158,7 @@ func (r *restoreCmd) Exec() error {
 	if err != nil {
 		return err
 	}
-	return r.restore(analyzedMD.LayersMetadata, groupFile, cacheStore)
+	return r.restore(analyzedMD.LayersMetadata, group, cacheStore)
 }
 
 func (r *restoreCmd) updateAnalyzedMD(analyzedMD *files.Analyzed, runImage imgutil.Image) error {
diff --git a/phase/exporter.go b/phase/exporter.go
index 004a041a..f3305751 100644
--- a/phase/exporter.go
+++ b/phase/exporter.go
@@ -123,8 +123,8 @@ func (e *Exporter) Export(opts ExportOptions) (files.Report, error) {
 	// ensure we always copy the new RunImage into the old stack to preserve old behavior
 	meta.Stack = &files.Stack{RunImage: opts.RunImageForExport}
 
-	buildMD := &files.BuildMetadata{}
-	if err := files.DecodeBuildMetadata(launch.GetMetadataFilePath(opts.LayersDir), e.PlatformAPI, buildMD); err != nil {
+	buildMD, err := files.Handler.ReadBuildMetadata(launch.GetMetadataFilePath(opts.LayersDir), e.PlatformAPI)
+	if err != nil {
 		return files.Report{}, errors.Wrap(err, "read build metadata")
 	}
 
diff --git a/phase/handlers.go b/phase/handlers.go
index 033edf50..5b6a5fad 100644
--- a/phase/handlers.go
+++ b/phase/handlers.go
@@ -1,18 +1,11 @@
 package phase
 
 import (
-	"fmt"
-
-	"github.com/BurntSushi/toml"
-
 	"github.com/buildpacks/lifecycle/buildpack"
-	"github.com/buildpacks/lifecycle/internal/encoding"
 	"github.com/buildpacks/lifecycle/log"
 	"github.com/buildpacks/lifecycle/platform/files"
 )
 
-var Config = &DefaultConfigHandler{}
-
 // CacheHandler wraps initialization of a cache image or cache volume.
 //
 //go:generate mockgen -package testmock -destination testmock/cache_handler.go github.com/buildpacks/lifecycle/phase CacheHandler
@@ -43,104 +36,8 @@ type BuildpackAPIVerifier interface {
 //go:generate mockgen -package testmock -destination testmock/config_handler.go github.com/buildpacks/lifecycle/phase ConfigHandler
 type ConfigHandler interface {
 	ReadAnalyzed(path string, logger log.Logger) (files.Analyzed, error)
-	ReadGroup(path string) (buildpackGroup []buildpack.GroupElement, extensionsGroup []buildpack.GroupElement, err error)
+	ReadGroup(path string) (buildpack.Group, error)
 	ReadOrder(path string) (buildpack.Order, buildpack.Order, error)
 	ReadRun(runPath string, logger log.Logger) (files.Run, error)
 	ReadPlan(path string) (files.Plan, error)
 }
-
-type DefaultConfigHandler struct{}
-
-func NewConfigHandler() *DefaultConfigHandler {
-	return &DefaultConfigHandler{}
-}
-
-// ReadAnalyzed reads the provided analyzed.toml file.
-func (h *DefaultConfigHandler) ReadAnalyzed(path string, logger log.Logger) (files.Analyzed, error) {
-	return files.ReadAnalyzed(path, logger)
-}
-
-// WriteAnalyzed writes the provided analyzed metadata to analyzed.toml.
-func (h *DefaultConfigHandler) WriteAnalyzed(path string, analyzedMD *files.Analyzed, logger log.Logger) error {
-	logger.Debugf("Run image info in analyzed metadata is: ")
-	logger.Debugf(encoding.ToJSONMaybe(analyzedMD.RunImage))
-	if err := encoding.WriteTOML(path, analyzedMD); err != nil {
-		return fmt.Errorf("failed to write analyzed: %w", err)
-	}
-	return nil
-}
-
-func (h *DefaultConfigHandler) ReadGroup(path string) (buildpackGroup []buildpack.GroupElement, extensionsGroup []buildpack.GroupElement, err error) {
-	var groupFile buildpack.Group
-	groupFile, err = ReadGroup(path)
-	if err != nil {
-		return nil, nil, fmt.Errorf("failed to read group file: %w", err)
-	}
-	return groupFile.Group, groupFile.GroupExtensions, nil
-}
-
-func ReadGroup(path string) (buildpack.Group, error) {
-	var group buildpack.Group
-	_, err := toml.DecodeFile(path, &group)
-	for e := range group.GroupExtensions {
-		group.GroupExtensions[e].Extension = true
-		group.GroupExtensions[e].Optional = true
-	}
-	return group, err
-}
-
-// WriteGroup writes the provided group information to group.toml.
-func (h *DefaultConfigHandler) WriteGroup(path string, group *buildpack.Group) error {
-	if err := encoding.WriteTOML(path, group); err != nil {
-		return fmt.Errorf("failed to write group: %w", err)
-	}
-	return nil
-}
-
-// ReadPlan reads the provided plan.toml file.
-func (h *DefaultConfigHandler) ReadPlan(path string) (files.Plan, error) {
-	var plan files.Plan
-	if _, err := toml.DecodeFile(path, &plan); err != nil {
-		return files.Plan{}, err
-	}
-	return plan, nil
-}
-
-// WritePlan writes the provided plan information to plan.toml.
-func (h *DefaultConfigHandler) WritePlan(path string, plan *files.Plan) error {
-	if err := encoding.WriteTOML(path, plan); err != nil {
-		return fmt.Errorf("failed to write plan: %w", err)
-	}
-	return nil
-}
-
-func (h *DefaultConfigHandler) ReadOrder(path string) (buildpack.Order, buildpack.Order, error) {
-	orderBp, orderExt, err := ReadOrder(path)
-	if err != nil {
-		return buildpack.Order{}, buildpack.Order{}, err
-	}
-	return orderBp, orderExt, nil
-}
-
-func ReadOrder(path string) (buildpack.Order, buildpack.Order, error) {
-	var order struct {
-		Order           buildpack.Order `toml:"order"`
-		OrderExtensions buildpack.Order `toml:"order-extensions"`
-	}
-	_, err := toml.DecodeFile(path, &order)
-	if err != nil {
-		return nil, nil, fmt.Errorf("failed to read order file: %w", err)
-	}
-	for g, group := range order.OrderExtensions {
-		for e := range group.Group {
-			group.Group[e].Extension = true
-			group.Group[e].Optional = true
-		}
-		order.OrderExtensions[g] = group
-	}
-	return order.Order, order.OrderExtensions, err
-}
-
-func (h *DefaultConfigHandler) ReadRun(runPath string, logger log.Logger) (files.Run, error) {
-	return files.ReadRun(runPath, logger)
-}
diff --git a/phase/hermetic_factory.go b/phase/hermetic_factory.go
index f3b1516b..7d772715 100644
--- a/phase/hermetic_factory.go
+++ b/phase/hermetic_factory.go
@@ -34,17 +34,14 @@ func NewHermeticFactory(
 }
 
 func (f *HermeticFactory) getExtensions(groupPath string, logger log.Logger) ([]buildpack.GroupElement, error) {
-	_, groupExt, err := f.configHandler.ReadGroup(groupPath)
+	group, err := f.configHandler.ReadGroup(groupPath)
 	if err != nil {
 		return nil, fmt.Errorf("reading group: %w", err)
 	}
-	for i := range groupExt {
-		groupExt[i].Extension = true
-	}
-	if err = f.verifyGroup(groupExt, logger); err != nil {
+	if err = f.verifyGroup(group.GroupExtensions, logger); err != nil {
 		return nil, err
 	}
-	return groupExt, nil
+	return group.GroupExtensions, nil
 }
 
 func (f *HermeticFactory) getOrder(path string, logger log.Logger) (order buildpack.Order, hasExtensions bool, err error) {
diff --git a/phase/rebaser.go b/phase/rebaser.go
index 7887332b..e6a02a7e 100644
--- a/phase/rebaser.go
+++ b/phase/rebaser.go
@@ -31,49 +31,46 @@ type Rebaser struct {
 	Force       bool
 }
 
-type RebaseReport struct {
-	Image files.ImageReport `toml:"image"`
-}
-
-func (r *Rebaser) Rebase(workingImage imgutil.Image, newBaseImage imgutil.Image, outputImageRef string, additionalNames []string) (RebaseReport, error) {
+// Rebase changes the underlying base image for an application image.
+func (r *Rebaser) Rebase(workingImage imgutil.Image, newBaseImage imgutil.Image, outputImageRef string, additionalNames []string) (files.RebaseReport, error) {
 	defer log.NewMeasurement("Rebaser", r.Logger)()
 	appPlatformAPI, err := workingImage.Env(platform.EnvPlatformAPI)
 	if err != nil {
-		return RebaseReport{}, fmt.Errorf("failed to get app image platform API: %w", err)
+		return files.RebaseReport{}, fmt.Errorf("failed to get app image platform API: %w", err)
 	}
 	// perform platform API-specific validations
 	if appPlatformAPI == "" || api.MustParse(appPlatformAPI).LessThan("0.12") {
 		if err = validateStackID(workingImage, newBaseImage); err != nil {
-			return RebaseReport{}, err
+			return files.RebaseReport{}, err
 		}
 		if err = validateMixins(workingImage, newBaseImage); err != nil {
-			return RebaseReport{}, err
+			return files.RebaseReport{}, err
 		}
 	} else {
 		if err = r.validateTarget(workingImage, newBaseImage); err != nil {
-			return RebaseReport{}, err
+			return files.RebaseReport{}, err
 		}
 	}
 
 	// get existing metadata label
 	var origMetadata files.LayersMetadataCompat
 	if err = image.DecodeLabel(workingImage, platform.LifecycleMetadataLabel, &origMetadata); err != nil {
-		return RebaseReport{}, fmt.Errorf("get image metadata: %w", err)
+		return files.RebaseReport{}, fmt.Errorf("get image metadata: %w", err)
 	}
 
 	// rebase
 	if err = workingImage.Rebase(origMetadata.RunImage.TopLayer, newBaseImage); err != nil {
-		return RebaseReport{}, fmt.Errorf("rebase app image: %w", err)
+		return files.RebaseReport{}, fmt.Errorf("rebase app image: %w", err)
 	}
 
 	// update metadata label
 	origMetadata.RunImage.TopLayer, err = newBaseImage.TopLayer()
 	if err != nil {
-		return RebaseReport{}, fmt.Errorf("get rebase run image top layer SHA: %w", err)
+		return files.RebaseReport{}, fmt.Errorf("get rebase run image top layer SHA: %w", err)
 	}
 	identifier, err := newBaseImage.Identifier()
 	if err != nil {
-		return RebaseReport{}, fmt.Errorf("get run image id or digest: %w", err)
+		return files.RebaseReport{}, fmt.Errorf("get run image id or digest: %w", err)
 	}
 	origMetadata.RunImage.Reference = identifier.String()
 	if r.PlatformAPI.AtLeast("0.12") {
@@ -100,7 +97,7 @@ func (r *Rebaser) Rebase(workingImage imgutil.Image, newBaseImage imgutil.Image,
 				newStackMD := origMetadata.RunImage.ToStack()
 				origMetadata.Stack = &newStackMD
 			} else {
-				return RebaseReport{}, fmt.Errorf(
+				return files.RebaseReport{}, fmt.Errorf(
 					msgRunImageMDNotContainsName+"; "+msgProvideForceToOverride,
 					newBaseImage.Name(),
 					existingRunImageMD,
@@ -112,10 +109,10 @@ func (r *Rebaser) Rebase(workingImage imgutil.Image, newBaseImage imgutil.Image,
 	// set metadata label
 	data, err := json.Marshal(origMetadata)
 	if err != nil {
-		return RebaseReport{}, fmt.Errorf("marshall metadata: %w", err)
+		return files.RebaseReport{}, fmt.Errorf("marshall metadata: %w", err)
 	}
 	if err := workingImage.SetLabel(platform.LifecycleMetadataLabel, string(data)); err != nil {
-		return RebaseReport{}, fmt.Errorf("set app image metadata label: %w", err)
+		return files.RebaseReport{}, fmt.Errorf("set app image metadata label: %w", err)
 	}
 
 	// update other labels
@@ -126,14 +123,14 @@ func (r *Rebaser) Rebase(workingImage imgutil.Image, newBaseImage imgutil.Image,
 		return strings.HasPrefix(l, "io.buildpacks.stack.")
 	}
 	if err := image.SyncLabels(newBaseImage, workingImage, hasPrefix); err != nil {
-		return RebaseReport{}, fmt.Errorf("set stack labels: %w", err)
+		return files.RebaseReport{}, fmt.Errorf("set stack labels: %w", err)
 	}
 
 	// save
-	report := RebaseReport{}
+	report := files.RebaseReport{}
 	report.Image, err = saveImageAs(workingImage, outputImageRef, additionalNames, r.Logger)
 	if err != nil {
-		return RebaseReport{}, err
+		return files.RebaseReport{}, err
 	}
 	return report, err
 }
diff --git a/platform/files/analyzed.go b/platform/files/analyzed.go
index 9eddedb5..b2b0709e 100644
--- a/platform/files/analyzed.go
+++ b/platform/files/analyzed.go
@@ -1,13 +1,8 @@
 package files
 
 import (
-	"os"
-
-	"github.com/BurntSushi/toml"
-
 	"github.com/buildpacks/lifecycle/buildpack"
 	"github.com/buildpacks/lifecycle/internal/encoding"
-	"github.com/buildpacks/lifecycle/log"
 )
 
 // Analyzed is written by the analyzer as analyzed.toml and updated in subsequent phases to record information about:
@@ -33,18 +28,6 @@ type Analyzed struct {
 	RunImage *RunImage `toml:"run-image,omitempty"`
 }
 
-func ReadAnalyzed(path string, logger log.Logger) (Analyzed, error) {
-	var analyzed Analyzed
-	if _, err := toml.DecodeFile(path, &analyzed); err != nil {
-		if os.IsNotExist(err) {
-			logger.Warnf("no analyzed metadata found at path '%s'", path)
-			return Analyzed{}, nil
-		}
-		return Analyzed{}, err
-	}
-	return analyzed, nil
-}
-
 func (a Analyzed) PreviousImageRef() string {
 	if a.PreviousImage == nil {
 		return ""
diff --git a/platform/files/handle_toml.go b/platform/files/handle_toml.go
new file mode 100644
index 00000000..4dc5032a
--- /dev/null
+++ b/platform/files/handle_toml.go
@@ -0,0 +1,197 @@
+// Package files contains schema and helper methods for working with lifecycle configuration files.
+package files
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/BurntSushi/toml"
+
+	"github.com/buildpacks/lifecycle/api"
+	"github.com/buildpacks/lifecycle/buildpack"
+	"github.com/buildpacks/lifecycle/internal/encoding"
+	"github.com/buildpacks/lifecycle/log"
+)
+
+// Handler is the default handler used to read and write lifecycle configuration files.
+var Handler = &TOMLHandler{}
+
+// TOMLHandler reads and writes lifecycle configuration files in TOML format.
+type TOMLHandler struct{}
+
+// NewHandler returns a new file handler.
+func NewHandler() *TOMLHandler {
+	return &TOMLHandler{}
+}
+
+// ReadAnalyzed reads the provided analyzed.toml file.
+// It logs a warning and returns empty analyzed metadata if the file does not exist.
+func (h *TOMLHandler) ReadAnalyzed(path string, logger log.Logger) (Analyzed, error) {
+	var analyzed Analyzed
+	if _, err := toml.DecodeFile(path, &analyzed); err != nil {
+		if os.IsNotExist(err) {
+			logger.Warnf("No analyzed metadata found at path %q", path)
+			return Analyzed{}, nil
+		}
+		return Analyzed{}, fmt.Errorf("failed to read analyzed file: %w", err)
+	}
+	return analyzed, nil
+}
+
+// WriteAnalyzed writes the provided analyzed metadata at the provided path.
+func (h *TOMLHandler) WriteAnalyzed(path string, analyzedMD *Analyzed, logger log.Logger) error {
+	logger.Debugf("Run image info in analyzed metadata is: ")
+	logger.Debugf(encoding.ToJSONMaybe(analyzedMD.RunImage))
+	if err := encoding.WriteTOML(path, analyzedMD); err != nil {
+		return fmt.Errorf("failed to write analyzed file: %w", err)
+	}
+	return nil
+}
+
+// ReadGroup reads the provided group.toml file.
+func (h *TOMLHandler) ReadGroup(path string) (group buildpack.Group, err error) {
+	if _, err = toml.DecodeFile(path, &group); err != nil {
+		return buildpack.Group{}, fmt.Errorf("failed to read group file: %w", err)
+	}
+	for e := range group.GroupExtensions {
+		group.GroupExtensions[e].Extension = true
+		group.GroupExtensions[e].Optional = true
+	}
+	return group, nil
+}
+
+// WriteGroup writes the provided group information at the provided path.
+func (h *TOMLHandler) WriteGroup(path string, group *buildpack.Group) error {
+	for e := range group.GroupExtensions {
+		group.GroupExtensions[e].Extension = false
+		group.GroupExtensions[e].Optional = false // avoid printing redundant information (extensions are always optional)
+	}
+	if err := encoding.WriteTOML(path, group); err != nil {
+		return fmt.Errorf("failed to write group file: %w", err)
+	}
+	return nil
+}
+
+// ReadBuildMetadata reads the provided metadata.toml file,
+// and sets the provided Platform API version on the returned struct so that the data can be re-encoded properly later.
+func (h *TOMLHandler) ReadBuildMetadata(path string, platformAPI *api.Version) (*BuildMetadata, error) {
+	buildMD := BuildMetadata{}
+	_, err := toml.DecodeFile(path, &buildMD)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read build metadata file: %w", err)
+	}
+	buildMD.PlatformAPI = platformAPI
+	for i, process := range buildMD.Processes {
+		buildMD.Processes[i] = process.WithPlatformAPI(platformAPI)
+	}
+	return &buildMD, nil
+}
+
+// WriteBuildMetadata writes the provided build metadata at the provided path.
+func (h *TOMLHandler) WriteBuildMetadata(path string, buildMD *BuildMetadata) error {
+	if err := encoding.WriteTOML(path, buildMD); err != nil {
+		return fmt.Errorf("failed to write build metadata file: %w", err)
+	}
+	return nil
+}
+
+// ReadOrder reads the provided order.toml file.
+func (h *TOMLHandler) ReadOrder(path string) (buildpack.Order, buildpack.Order, error) {
+	orderBp, orderExt, err := readOrder(path)
+	if err != nil {
+		return buildpack.Order{}, buildpack.Order{}, err
+	}
+	return orderBp, orderExt, nil
+}
+
+func readOrder(path string) (buildpack.Order, buildpack.Order, error) {
+	var order struct {
+		Order           buildpack.Order `toml:"order"`
+		OrderExtensions buildpack.Order `toml:"order-extensions"`
+	}
+	_, err := toml.DecodeFile(path, &order)
+	if err != nil {
+		return nil, nil, fmt.Errorf("failed to read order file: %w", err)
+	}
+	for g, group := range order.OrderExtensions {
+		for e := range group.Group {
+			group.Group[e].Extension = true
+			group.Group[e].Optional = true
+		}
+		order.OrderExtensions[g] = group
+	}
+	return order.Order, order.OrderExtensions, nil
+}
+
+// ReadPlan reads the provided plan.toml file.
+func (h *TOMLHandler) ReadPlan(path string) (Plan, error) {
+	var plan Plan
+	if _, err := toml.DecodeFile(path, &plan); err != nil {
+		return Plan{}, fmt.Errorf("failed to read plan file: %w", err)
+	}
+	return plan, nil
+}
+
+// WritePlan writes the provided plan information at the provided path.
+func (h *TOMLHandler) WritePlan(path string, plan *Plan) error {
+	if err := encoding.WriteTOML(path, plan); err != nil {
+		return fmt.Errorf("failed to write plan file: %w", err)
+	}
+	return nil
+}
+
+// ReadProjectMetadata reads the provided project_metadata.toml file.
+// It logs a warning and returns empty project metadata if the file does not exist.
+func (h *TOMLHandler) ReadProjectMetadata(path string, logger log.Logger) (ProjectMetadata, error) {
+	var projectMD ProjectMetadata
+	if _, err := toml.DecodeFile(path, &projectMD); err != nil {
+		if os.IsNotExist(err) {
+			logger.Debugf("No project metadata found at path %q, project metadata will not be exported", path)
+			return ProjectMetadata{}, nil
+		}
+		return ProjectMetadata{}, fmt.Errorf("failed to read project metadata file: %w", err)
+	}
+	return projectMD, nil
+}
+
+// WriteReport writes the provided report information at the provided path.
+func (h *TOMLHandler) WriteReport(path string, report *Report) error {
+	if err := encoding.WriteTOML(path, report); err != nil {
+		return fmt.Errorf("failed to write report file: %w", err)
+	}
+	return nil
+}
+
+// WriteRebaseReport writes the provided report information at the provided path.
+func (h *TOMLHandler) WriteRebaseReport(path string, report *RebaseReport) error {
+	if err := encoding.WriteTOML(path, report); err != nil {
+		return fmt.Errorf("failed to write report file: %w", err)
+	}
+	return nil
+}
+
+// ReadRun reads the provided run.toml file.
+func (h *TOMLHandler) ReadRun(path string, logger log.Logger) (Run, error) {
+	var runMD Run
+	if _, err := toml.DecodeFile(path, &runMD); err != nil {
+		if os.IsNotExist(err) {
+			logger.Infof("No run metadata found at path %q", path)
+			return Run{}, nil
+		}
+		return Run{}, fmt.Errorf("failed to read run file: %w", err)
+	}
+	return runMD, nil
+}
+
+// ReadStack reads the provided stack.toml file.
+func (h *TOMLHandler) ReadStack(path string, logger log.Logger) (Stack, error) {
+	var stackMD Stack
+	if _, err := toml.DecodeFile(path, &stackMD); err != nil {
+		if os.IsNotExist(err) {
+			logger.Infof("No stack metadata found at path %q", path)
+			return Stack{}, nil
+		}
+		return Stack{}, fmt.Errorf("failed to read stack file: %w", err)
+	}
+	return stackMD, nil
+}
diff --git a/platform/files/metadata.go b/platform/files/metadata.go
index 95eee226..e023e8b3 100644
--- a/platform/files/metadata.go
+++ b/platform/files/metadata.go
@@ -3,8 +3,6 @@ package files
 import (
 	"encoding/json"
 
-	"github.com/BurntSushi/toml"
-
 	"github.com/buildpacks/lifecycle/api"
 	"github.com/buildpacks/lifecycle/buildpack"
 	"github.com/buildpacks/lifecycle/launch"
@@ -36,25 +34,6 @@ type BuildMetadata struct {
 	PlatformAPI *api.Version `toml:"-" json:"-"`
 }
 
-// DecodeBuildMetadata reads a metadata.toml file
-func DecodeBuildMetadata(path string, platformAPI *api.Version, buildmd *BuildMetadata) error {
-	// decode the common bits
-	_, err := toml.DecodeFile(path, &buildmd)
-	if err != nil {
-		return err
-	}
-
-	// set the platform API on all the appropriate fields
-	// this will allow us to re-encode the metadata.toml file with
-	// the current platform API
-	buildmd.PlatformAPI = platformAPI
-	for i, process := range buildmd.Processes {
-		buildmd.Processes[i] = process.WithPlatformAPI(platformAPI)
-	}
-
-	return nil
-}
-
 func (m *BuildMetadata) MarshalJSON() ([]byte, error) {
 	if m.PlatformAPI == nil || m.PlatformAPI.LessThan("0.9") {
 		return json.Marshal(*m)
diff --git a/platform/files/report.go b/platform/files/report.go
index 00b5404b..c29e8650 100644
--- a/platform/files/report.go
+++ b/platform/files/report.go
@@ -2,7 +2,7 @@ package files
 
 import "github.com/buildpacks/lifecycle/buildpack"
 
-// Report is written by the exporter as report.toml to record information about the build.
+// Report is written by the exporter to record information about the build.
 // It is not included in the output image, but can be saved off by the platform before the build container exits.
 // The location of the file can be specified by providing `-report <path>` to the lifecycle.
 type Report struct {
@@ -20,3 +20,8 @@ type ImageReport struct {
 	Digest       string   `toml:"digest,omitempty"`
 	ManifestSize int64    `toml:"manifest-size,omitzero"`
 }
+
+// RebaseReport is written by the rebaser to record information about the rebased image.
+type RebaseReport struct {
+	Image ImageReport `toml:"image"`
+}
diff --git a/platform/files/run.go b/platform/files/run.go
index 84866745..00fbe038 100644
--- a/platform/files/run.go
+++ b/platform/files/run.go
@@ -1,13 +1,5 @@
 package files
 
-import (
-	"os"
-
-	"github.com/BurntSushi/toml"
-
-	"github.com/buildpacks/lifecycle/log"
-)
-
 // Run is provided by the platform as run.toml to record information about the run images
 // that may be used during export.
 // Data from the selected run image is serialized by the exporter as the `runImage` key in the `io.buildpacks.lifecycle.metadata` label
@@ -27,15 +19,3 @@ func (r *Run) Contains(providedImage string) bool {
 	}
 	return false
 }
-
-func ReadRun(runPath string, logger log.Logger) (Run, error) {
-	var runMD Run
-	if _, err := toml.DecodeFile(runPath, &runMD); err != nil {
-		if os.IsNotExist(err) {
-			logger.Infof("no run metadata found at path '%s'\n", runPath)
-			return Run{}, nil
-		}
-		return Run{}, err
-	}
-	return runMD, nil
-}
diff --git a/platform/files/stack.go b/platform/files/stack.go
index 7931d834..70ed450c 100644
--- a/platform/files/stack.go
+++ b/platform/files/stack.go
@@ -1,12 +1,7 @@
 package files
 
 import (
-	"os"
-
-	"github.com/BurntSushi/toml"
-
 	iname "github.com/buildpacks/lifecycle/internal/name"
-	"github.com/buildpacks/lifecycle/log"
 )
 
 // Stack (deprecated as of Platform API 0.12) is provided by the platform as stack.toml to record information about the run images
@@ -37,15 +32,3 @@ func (r *RunImageForExport) Contains(providedImage string) bool {
 	}
 	return false
 }
-
-func ReadStack(stackPath string, logger log.Logger) (Stack, error) {
-	var stackMD Stack
-	if _, err := toml.DecodeFile(stackPath, &stackMD); err != nil {
-		if os.IsNotExist(err) {
-			logger.Infof("no stack metadata found at path '%s'\n", stackPath)
-			return Stack{}, nil
-		}
-		return Stack{}, err
-	}
-	return stackMD, nil
-}
diff --git a/platform/resolve_inputs.go b/platform/resolve_inputs.go
index 68d633be..257b88ee 100644
--- a/platform/resolve_inputs.go
+++ b/platform/resolve_inputs.go
@@ -133,7 +133,7 @@ func FillExportRunImage(i *LifecycleInputs, logger log.Logger) error {
 	case i.DeprecatedRunImageRef != "":
 		return errors.New(ErrImageUnsupported)
 	default:
-		analyzedMD, err := files.ReadAnalyzed(i.AnalyzedPath, logger)
+		analyzedMD, err := files.Handler.ReadAnalyzed(i.AnalyzedPath, logger)
 		if err != nil {
 			return err
 		}
@@ -156,7 +156,7 @@ func fillRunImageFromRunTOMLIfNeeded(i *LifecycleInputs, logger log.Logger) erro
 	if err != nil {
 		return err
 	}
-	runMD, err := files.ReadRun(i.RunPath, logger)
+	runMD, err := files.Handler.ReadRun(i.RunPath, logger)
 	if err != nil {
 		return err
 	}
@@ -177,7 +177,7 @@ func fillRunImageFromStackTOMLIfNeeded(i *LifecycleInputs, logger log.Logger) er
 	if err != nil {
 		return err
 	}
-	stackMD, err := files.ReadStack(i.StackPath, logger)
+	stackMD, err := files.Handler.ReadStack(i.StackPath, logger)
 	if err != nil {
 		return err
 	}
diff --git a/platform/run_image.go b/platform/run_image.go
index 12cc283b..c80e6b28 100644
--- a/platform/run_image.go
+++ b/platform/run_image.go
@@ -80,20 +80,20 @@ func byRegistry(reg string, images []string, checkReadAccess CheckReadAccess, ke
 // as the run image "reference" could be a daemon image ID (which we'd not expect to find in run.toml).
 func GetRunImageForExport(inputs LifecycleInputs) (files.RunImageForExport, error) {
 	if inputs.PlatformAPI.LessThan("0.12") {
-		stackMD, err := files.ReadStack(inputs.StackPath, cmd.DefaultLogger)
+		stackMD, err := files.Handler.ReadStack(inputs.StackPath, cmd.DefaultLogger)
 		if err != nil {
 			return files.RunImageForExport{}, err
 		}
 		return stackMD.RunImage, nil
 	}
-	runMD, err := files.ReadRun(inputs.RunPath, cmd.DefaultLogger)
+	runMD, err := files.Handler.ReadRun(inputs.RunPath, cmd.DefaultLogger)
 	if err != nil {
 		return files.RunImageForExport{}, err
 	}
 	if len(runMD.Images) == 0 {
 		return files.RunImageForExport{}, nil
 	}
-	analyzedMD, err := files.ReadAnalyzed(inputs.AnalyzedPath, cmd.DefaultLogger)
+	analyzedMD, err := files.Handler.ReadAnalyzed(inputs.AnalyzedPath, cmd.DefaultLogger)
 	if err != nil {
 		return files.RunImageForExport{}, err
 	}
@@ -102,8 +102,8 @@ func GetRunImageForExport(inputs LifecycleInputs) (files.RunImageForExport, erro
 			return runImage, nil
 		}
 	}
-	buildMD := &files.BuildMetadata{}
-	if err = files.DecodeBuildMetadata(launch.GetMetadataFilePath(inputs.LayersDir), inputs.PlatformAPI, buildMD); err != nil {
+	buildMD, err := files.Handler.ReadBuildMetadata(launch.GetMetadataFilePath(inputs.LayersDir), inputs.PlatformAPI)
+	if err != nil {
 		return files.RunImageForExport{}, err
 	}
 	if len(buildMD.Extensions) > 0 { // FIXME: try to know for sure if extensions were used to switch the run image


Test patch:
diff --git a/acceptance/analyzer_test.go b/acceptance/analyzer_test.go
index aca6b1af..ec1a232f 100644
--- a/acceptance/analyzer_test.go
+++ b/acceptance/analyzer_test.go
@@ -521,7 +521,7 @@ func assertAnalyzedMetadata(t *testing.T, path string) *files.Analyzed {
 	h.AssertNil(t, err)
 	h.AssertEq(t, len(contents) > 0, true)
 
-	analyzedMD, err := files.ReadAnalyzed(path, cmd.DefaultLogger)
+	analyzedMD, err := files.Handler.ReadAnalyzed(path, cmd.DefaultLogger)
 	h.AssertNil(t, err)
 
 	return &analyzedMD
diff --git a/acceptance/builder_test.go b/acceptance/builder_test.go
index 4e5f7763..eae15268 100644
--- a/acceptance/builder_test.go
+++ b/acceptance/builder_test.go
@@ -9,7 +9,6 @@ import (
 	"runtime"
 	"testing"
 
-	"github.com/BurntSushi/toml"
 	"github.com/sclevine/spec"
 	"github.com/sclevine/spec/report"
 
@@ -221,7 +220,6 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) {
 			h.AssertStringContains(t, md.Buildpacks[0].ID, "hello_world")
 			h.AssertStringContains(t, md.Buildpacks[0].Version, "0.0.1")
 			h.AssertStringContains(t, md.Extensions[0].API, "0.10")
-			h.AssertEq(t, md.Extensions[0].Extension, false) // this shows that `extension = true` is not redundantly printed in group.toml
 			h.AssertStringContains(t, md.Extensions[0].ID, "hello_world")
 			h.AssertStringContains(t, md.Extensions[0].Version, "0.0.1")
 		})
@@ -242,7 +240,7 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) {
 					)
 					output, err := command.CombinedOutput()
 					h.AssertNotNil(t, err)
-					expected := "failed to read buildpack group: open /layers/group.toml: no such file or directory"
+					expected := "failed to read group file: open /layers/group.toml: no such file or directory"
 					h.AssertStringContains(t, string(output), expected)
 				})
 			})
@@ -279,7 +277,7 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) {
 					)
 					output, err := command.CombinedOutput()
 					h.AssertNotNil(t, err)
-					expected := "failed to read buildpack group: toml: line 1: expected '.' or '=', but got 'a' instead"
+					expected := "failed to read group file: toml: line 1: expected '.' or '=', but got 'a' instead"
 					h.AssertStringContains(t, string(output), expected)
 				})
 			})
@@ -318,7 +316,7 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) {
 					)
 					output, err := command.CombinedOutput()
 					h.AssertNotNil(t, err)
-					expected := "failed to parse detect plan: open /layers/plan.toml: no such file or directory"
+					expected := "failed to read plan file: open /layers/plan.toml: no such file or directory"
 					h.AssertStringContains(t, string(output), expected)
 				})
 			})
@@ -358,7 +356,7 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) {
 					)
 					output, err := command.CombinedOutput()
 					h.AssertNotNil(t, err)
-					expected := "failed to parse detect plan: toml: line 1: expected '.' or '=', but got 'a' instead"
+					expected := "failed to read plan file: toml: line 1: expected '.' or '=', but got 'a' instead"
 					h.AssertStringContains(t, string(output), expected)
 				})
 			})
@@ -533,9 +531,8 @@ func getBuilderMetadata(t *testing.T, path string) (string, *files.BuildMetadata
 	contents, _ := os.ReadFile(path)
 	h.AssertEq(t, len(contents) > 0, true)
 
-	var buildMD files.BuildMetadata
-	_, err := toml.Decode(string(contents), &buildMD)
+	buildMD, err := files.Handler.ReadBuildMetadata(path, api.MustParse(latestPlatformAPI))
 	h.AssertNil(t, err)
 
-	return string(contents), &buildMD
+	return string(contents), buildMD
 }
diff --git a/acceptance/detector_test.go b/acceptance/detector_test.go
index 91b97a53..aeb5794f 100644
--- a/acceptance/detector_test.go
+++ b/acceptance/detector_test.go
@@ -12,12 +12,11 @@ import (
 	"runtime"
 	"testing"
 
-	"github.com/BurntSushi/toml"
 	"github.com/sclevine/spec"
 	"github.com/sclevine/spec/report"
 
 	"github.com/buildpacks/lifecycle/api"
-	"github.com/buildpacks/lifecycle/buildpack"
+	"github.com/buildpacks/lifecycle/cmd"
 	"github.com/buildpacks/lifecycle/platform/files"
 	h "github.com/buildpacks/lifecycle/testhelpers"
 )
@@ -170,16 +169,14 @@ fail: fail_detect_buildpack@some_version
 
 			// check group.toml
 			foundGroupTOML := filepath.Join(copyDir, "layers", "group.toml")
-			var buildpackGroup buildpack.Group
-			_, err := toml.DecodeFile(foundGroupTOML, &buildpackGroup)
+			group, err := files.Handler.ReadGroup(foundGroupTOML)
 			h.AssertNil(t, err)
-			h.AssertEq(t, buildpackGroup.Group[0].ID, "simple_buildpack")
-			h.AssertEq(t, buildpackGroup.Group[0].Version, "simple_buildpack_version")
+			h.AssertEq(t, group.Group[0].ID, "simple_buildpack")
+			h.AssertEq(t, group.Group[0].Version, "simple_buildpack_version")
 
 			// check plan.toml
-			tempPlanToml := filepath.Join(copyDir, "layers", "plan.toml")
-			var buildPlan files.Plan
-			_, err = toml.DecodeFile(tempPlanToml, &buildPlan)
+			foundPlanTOML := filepath.Join(copyDir, "layers", "plan.toml")
+			buildPlan, err := files.Handler.ReadPlan(foundPlanTOML)
 			h.AssertNil(t, err)
 			h.AssertEq(t, buildPlan.Entries[0].Providers[0].ID, "simple_buildpack")
 			h.AssertEq(t, buildPlan.Entries[0].Providers[0].Version, "simple_buildpack_version")
@@ -230,11 +227,10 @@ fail: fail_detect_buildpack@some_version
 
 			// check group.toml
 			foundGroupTOML := filepath.Join(copyDir, "layers", "custom_group.toml")
-			var buildpackGroup buildpack.Group
-			_, err := toml.DecodeFile(foundGroupTOML, &buildpackGroup)
+			group, err := files.Handler.ReadGroup(foundGroupTOML)
 			h.AssertNil(t, err)
-			h.AssertEq(t, buildpackGroup.Group[0].ID, "always_detect_buildpack")
-			h.AssertEq(t, buildpackGroup.Group[0].Version, "always_detect_buildpack_version")
+			h.AssertEq(t, group.Group[0].ID, "always_detect_buildpack")
+			h.AssertEq(t, group.Group[0].Version, "always_detect_buildpack_version")
 
 			// check plan.toml - should be empty since we're using always_detect_order.toml so there is no "actual plan"
 			tempPlanToml := filepath.Join(copyDir, "layers", "custom_plan.toml")
@@ -291,11 +287,10 @@ fail: fail_detect_buildpack@some_version
 
 				// check group.toml
 				foundGroupTOML := filepath.Join(copyDir, "layers", "group.toml")
-				var buildpackGroup buildpack.Group
-				_, err := toml.DecodeFile(foundGroupTOML, &buildpackGroup)
+				group, err := files.Handler.ReadGroup(foundGroupTOML)
 				h.AssertNil(t, err)
-				h.AssertEq(t, buildpackGroup.Group[0].ID, "simple_buildpack")
-				h.AssertEq(t, buildpackGroup.Group[0].Version, "simple_buildpack_version")
+				h.AssertEq(t, group.Group[0].ID, "simple_buildpack")
+				h.AssertEq(t, group.Group[0].Version, "simple_buildpack_version")
 			})
 		})
 
@@ -381,21 +376,18 @@ fail: fail_detect_buildpack@some_version
 			h.AssertStringContains(t, output, "simple_extension: output from /bin/detect")
 			t.Log("writes group.toml")
 			foundGroupTOML := filepath.Join(copyDir, "layers", "group.toml")
-			var buildpackGroup buildpack.Group
-			_, err := toml.DecodeFile(foundGroupTOML, &buildpackGroup)
+			group, err := files.Handler.ReadGroup(foundGroupTOML)
 			h.AssertNil(t, err)
-			h.AssertEq(t, buildpackGroup.GroupExtensions[0].ID, "simple_extension")
-			h.AssertEq(t, buildpackGroup.GroupExtensions[0].Version, "simple_extension_version")
-			h.AssertEq(t, buildpackGroup.GroupExtensions[0].Extension, false) // this shows that `extension = true` is not redundantly printed in group.toml
-			h.AssertEq(t, buildpackGroup.Group[0].ID, "buildpack_for_ext")
-			h.AssertEq(t, buildpackGroup.Group[0].Version, "buildpack_for_ext_version")
-			h.AssertEq(t, buildpackGroup.Group[0].Extension, false)
+			h.AssertEq(t, group.GroupExtensions[0].ID, "simple_extension")
+			h.AssertEq(t, group.GroupExtensions[0].Version, "simple_extension_version")
+			h.AssertEq(t, group.Group[0].ID, "buildpack_for_ext")
+			h.AssertEq(t, group.Group[0].Version, "buildpack_for_ext_version")
+			h.AssertEq(t, group.Group[0].Extension, false)
 			t.Log("writes plan.toml")
 			foundPlanTOML := filepath.Join(copyDir, "layers", "plan.toml")
-			var plan files.Plan
-			_, err = toml.DecodeFile(foundPlanTOML, &plan)
+			buildPlan, err := files.Handler.ReadPlan(foundPlanTOML)
 			h.AssertNil(t, err)
-			h.AssertEq(t, len(plan.Entries), 0) // this shows that the plan was filtered to remove `requires` provided by extensions
+			h.AssertEq(t, len(buildPlan.Entries), 0) // this shows that the plan was filtered to remove `requires` provided by extensions
 
 			t.Log("runs /bin/generate for extensions")
 			h.AssertStringContains(t, output, "simple_extension: output from /bin/generate")
@@ -406,10 +398,9 @@ fail: fail_detect_buildpack@some_version
 			h.AssertEq(t, string(contents), "FROM some-run-image-from-extension\n")
 			t.Log("records the new run image in analyzed.toml")
 			foundAnalyzedTOML := filepath.Join(copyDir, "layers", "analyzed.toml")
-			var analyzed files.Analyzed
-			_, err = toml.DecodeFile(foundAnalyzedTOML, &analyzed)
+			analyzedMD, err := files.Handler.ReadAnalyzed(foundAnalyzedTOML, cmd.DefaultLogger)
 			h.AssertNil(t, err)
-			h.AssertEq(t, analyzed.RunImage.Image, "some-run-image-from-extension")
+			h.AssertEq(t, analyzedMD.RunImage.Image, "some-run-image-from-extension")
 		})
 	})
 }
diff --git a/acceptance/extender_test.go b/acceptance/extender_test.go
index f6c31079..4ea54a01 100644
--- a/acceptance/extender_test.go
+++ b/acceptance/extender_test.go
@@ -21,7 +21,7 @@ import (
 
 	"github.com/buildpacks/lifecycle/api"
 	"github.com/buildpacks/lifecycle/auth"
-	"github.com/buildpacks/lifecycle/internal/encoding"
+	"github.com/buildpacks/lifecycle/cmd"
 	"github.com/buildpacks/lifecycle/internal/selective"
 	"github.com/buildpacks/lifecycle/platform/files"
 	h "github.com/buildpacks/lifecycle/testhelpers"
@@ -120,7 +120,7 @@ func testExtenderFunc(platformAPI string) func(t *testing.T, when spec.G, it spe
 					},
 				}
 				analyzedPath = h.TempFile(t, "", "analyzed.toml")
-				h.AssertNil(t, encoding.WriteTOML(analyzedPath, analyzedMD))
+				h.AssertNil(t, files.Handler.WriteAnalyzed(analyzedPath, &analyzedMD, cmd.DefaultLogger))
 			})
 
 			it.After(func() {
diff --git a/acceptance/phase_test.go b/acceptance/phase_test.go
index 10ed0ba9..47c8d0f7 100644
--- a/acceptance/phase_test.go
+++ b/acceptance/phase_test.go
@@ -2,6 +2,8 @@ package acceptance
 
 import (
 	"context"
+	"crypto/sha256"
+	"encoding/hex"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -15,12 +17,12 @@ import (
 	"testing"
 	"time"
 
-	"github.com/BurntSushi/toml"
 	ih "github.com/buildpacks/imgutil/testhelpers"
 	"github.com/google/go-containerregistry/pkg/authn"
 	"github.com/google/go-containerregistry/pkg/registry"
 
 	"github.com/buildpacks/lifecycle/auth"
+	"github.com/buildpacks/lifecycle/cmd"
 	"github.com/buildpacks/lifecycle/internal/encoding"
 	"github.com/buildpacks/lifecycle/platform"
 	"github.com/buildpacks/lifecycle/platform/files"
@@ -126,6 +128,14 @@ func (p *PhaseTest) Start(t *testing.T, phaseOp ...func(*testing.T, *PhaseTest))
 	}
 
 	h.MakeAndCopyLifecycle(t, p.targetDaemon.os, p.targetDaemon.arch, p.containerBinaryDir)
+	// calculate lifecycle digest
+	hasher := sha256.New()
+	f, err := os.Open(filepath.Join(p.containerBinaryDir, "lifecycle"+exe)) //#nosec G304
+	h.AssertNil(t, err)
+	_, err = io.Copy(hasher, f)
+	h.AssertNil(t, err)
+	t.Logf("Built lifecycle binary with digest: %s", hex.EncodeToString(hasher.Sum(nil)))
+
 	copyFakeSboms(t)
 	h.DockerBuild(
 		t,
@@ -133,6 +143,14 @@ func (p *PhaseTest) Start(t *testing.T, phaseOp ...func(*testing.T, *PhaseTest))
 		p.testImageDockerContext,
 		h.WithArgs("-f", filepath.Join(p.testImageDockerContext, dockerfileName)),
 	)
+	t.Logf("Using image %s with lifecycle version %s",
+		p.testImageRef,
+		h.DockerRun(
+			t,
+			p.testImageRef,
+			h.WithFlags("--env", "CNB_PLATFORM_API="+latestPlatformAPI, "--entrypoint", ctrPath("/cnb/lifecycle/lifecycle"+exe)),
+			h.WithArgs("-version"),
+		))
 }
 
 func (p *PhaseTest) Stop(t *testing.T) {
@@ -427,8 +445,7 @@ func assertRunMetadata(t *testing.T, path string) *files.Run { //nolint
 	h.AssertNil(t, err)
 	h.AssertEq(t, len(contents) > 0, true)
 
-	var runMD files.Run
-	_, err = toml.Decode(string(contents), &runMD)
+	runMD, err := files.Handler.ReadRun(path, cmd.DefaultLogger)
 	h.AssertNil(t, err)
 
 	return &runMD
diff --git a/acceptance/restorer_test.go b/acceptance/restorer_test.go
index 52eb22c4..02f4b471 100644
--- a/acceptance/restorer_test.go
+++ b/acceptance/restorer_test.go
@@ -16,7 +16,7 @@ import (
 
 	"github.com/buildpacks/lifecycle/api"
 	"github.com/buildpacks/lifecycle/cmd"
-	"github.com/buildpacks/lifecycle/phase"
+	"github.com/buildpacks/lifecycle/platform/files"
 	h "github.com/buildpacks/lifecycle/testhelpers"
 )
 
@@ -198,7 +198,7 @@ func testRestorerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe
 					h.WithArgs("-build-image", restoreRegFixtures.SomeCacheImage), // some-cache-image simulates a builder image in a registry
 				)
 				t.Log("records builder image digest in analyzed.toml")
-				analyzedMD, err := phase.Config.ReadAnalyzed(filepath.Join(copyDir, "layers", "analyzed.toml"), cmd.DefaultLogger)
+				analyzedMD, err := files.Handler.ReadAnalyzed(filepath.Join(copyDir, "layers", "analyzed.toml"), cmd.DefaultLogger)
 				h.AssertNil(t, err)
 				h.AssertStringContains(t, analyzedMD.BuildImage.Reference, restoreRegFixtures.SomeCacheImage+"@sha256:")
 				t.Log("writes builder manifest and config to the kaniko cache")
@@ -230,7 +230,7 @@ func testRestorerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe
 					),
 				)
 				t.Log("updates run image reference in analyzed.toml to include digest and target data")
-				analyzedMD, err := phase.Config.ReadAnalyzed(filepath.Join(copyDir, "layers", "some-extend-true-analyzed.toml"), cmd.DefaultLogger)
+				analyzedMD, err := files.Handler.ReadAnalyzed(filepath.Join(copyDir, "layers", "some-extend-true-analyzed.toml"), cmd.DefaultLogger)
 				h.AssertNil(t, err)
 				h.AssertStringContains(t, analyzedMD.RunImage.Reference, restoreRegFixtures.ReadOnlyRunImage+"@sha256:")
 				h.AssertEq(t, analyzedMD.RunImage.Image, restoreRegFixtures.ReadOnlyRunImage)
@@ -267,7 +267,7 @@ func testRestorerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe
 				)
 				if api.MustParse(platformAPI).AtLeast("0.12") {
 					t.Log("updates run image reference in analyzed.toml to include digest and target data")
-					analyzedMD, err := phase.Config.ReadAnalyzed(filepath.Join(copyDir, "layers", "some-extend-false-analyzed.toml"), cmd.DefaultLogger)
+					analyzedMD, err := files.Handler.ReadAnalyzed(filepath.Join(copyDir, "layers", "some-extend-false-analyzed.toml"), cmd.DefaultLogger)
 					h.AssertNil(t, err)
 					h.AssertStringContains(t, analyzedMD.RunImage.Reference, restoreRegFixtures.ReadOnlyRunImage+"@sha256:")
 					h.AssertEq(t, analyzedMD.RunImage.Image, restoreRegFixtures.ReadOnlyRunImage)
@@ -280,7 +280,7 @@ func testRestorerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe
 					h.AssertEq(t, len(fis), 1) // .gitkeep
 				} else {
 					t.Log("updates run image reference in analyzed.toml to include digest only")
-					analyzedMD, err := phase.Config.ReadAnalyzed(filepath.Join(copyDir, "layers", "some-extend-false-analyzed.toml"), cmd.DefaultLogger)
+					analyzedMD, err := files.Handler.ReadAnalyzed(filepath.Join(copyDir, "layers", "some-extend-false-analyzed.toml"), cmd.DefaultLogger)
 					h.AssertNil(t, err)
 					h.AssertStringContains(t, analyzedMD.RunImage.Reference, restoreRegFixtures.ReadOnlyRunImage+"@sha256:")
 					h.AssertEq(t, analyzedMD.RunImage.Image, restoreRegFixtures.ReadOnlyRunImage)
@@ -311,7 +311,7 @@ func testRestorerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe
 						),
 					)
 					t.Log("updates run image reference in analyzed.toml to include digest and target data")
-					analyzedMD, err := phase.Config.ReadAnalyzed(filepath.Join(copyDir, "layers", "some-extend-false-analyzed.toml"), cmd.DefaultLogger)
+					analyzedMD, err := files.Handler.ReadAnalyzed(filepath.Join(copyDir, "layers", "some-extend-false-analyzed.toml"), cmd.DefaultLogger)
 					h.AssertNil(t, err)
 					h.AssertStringDoesNotContain(t, analyzedMD.RunImage.Reference, "@sha256:") // daemon image ID
 					h.AssertEq(t, analyzedMD.RunImage.Image, restoreRegFixtures.ReadOnlyRunImage)
diff --git a/phase/extender_test.go b/phase/extender_test.go
index 50e13ff7..f0c2bd5f 100644
--- a/phase/extender_test.go
+++ b/phase/extender_test.go
@@ -59,7 +59,7 @@ func testExtenderFactory(t *testing.T, when spec.G, it spec.S) {
 				analyzedMD, nil,
 			).AnyTimes()
 			fakeConfigHandler.EXPECT().ReadGroup("some-group-path").Return(
-				[]buildpack.GroupElement{}, []buildpack.GroupElement{{ID: "A", Version: "v1", API: "0.9"}}, nil,
+				buildpack.Group{GroupExtensions: []buildpack.GroupElement{{ID: "A", Version: "v1", API: "0.9", Extension: true}}}, nil,
 			).AnyTimes()
 			fakeDirStore.EXPECT().LookupExt("A", "v1").Return(&buildpack.ExtDescriptor{
 				WithAPI: "0.9",
diff --git a/phase/generator_test.go b/phase/generator_test.go
index 7eb7f57f..d77371e7 100644
--- a/phase/generator_test.go
+++ b/phase/generator_test.go
@@ -69,9 +69,9 @@ func testGeneratorFactory(t *testing.T, when spec.G, it spec.S) {
 			fakeConfigHandler.EXPECT().ReadAnalyzed("some-analyzed-path", logger).Return(files.Analyzed{RunImage: &files.RunImage{Reference: "some-run-image-ref"}}, nil)
 			fakeConfigHandler.EXPECT().ReadRun("some-run-path", logger).Return(files.Run{Images: []files.RunImageForExport{{Image: "some-run-image"}}}, nil)
 			providedExtensions := []buildpack.GroupElement{
-				{ID: "A", Version: "v1", API: "0.9"},
+				{ID: "A", Version: "v1", API: "0.9", Extension: true},
 			}
-			fakeConfigHandler.EXPECT().ReadGroup("some-group-path").Return([]buildpack.GroupElement{}, providedExtensions, nil)
+			fakeConfigHandler.EXPECT().ReadGroup("some-group-path").Return(buildpack.Group{GroupExtensions: providedExtensions}, nil)
 			providedPlan := files.Plan{Entries: []files.BuildPlanEntry{
 				{
 					Providers: []buildpack.GroupElement{
diff --git a/phase/handlers_test.go b/phase/handlers_test.go
index 6518f67d..1b13f110 100644
--- a/phase/handlers_test.go
+++ b/phase/handlers_test.go
@@ -9,7 +9,7 @@ import (
 	"github.com/sclevine/spec/report"
 
 	"github.com/buildpacks/lifecycle/buildpack"
-	"github.com/buildpacks/lifecycle/phase"
+	"github.com/buildpacks/lifecycle/platform/files"
 	h "github.com/buildpacks/lifecycle/testhelpers"
 )
 
@@ -80,19 +80,17 @@ group = [{id = "D", version = "v1"}]
 	when("#ReadGroup", func() {
 		it("returns a single group object with a buildpack group and an extensions group", func() {
 			h.Mkfile(t, groupTOMLContents, filepath.Join(tmpDir, "group.toml"))
-			foundGroup, err := phase.ReadGroup(filepath.Join(tmpDir, "group.toml"))
+			group, err := files.Handler.ReadGroup(filepath.Join(tmpDir, "group.toml"))
 			h.AssertNil(t, err)
-			h.AssertEq(t, foundGroup, buildpack.Group{
-				Group:           expectedGroupBp,
-				GroupExtensions: expectedGroupExt,
-			})
+			h.AssertEq(t, group.Group, expectedGroupBp)
+			h.AssertEq(t, group.GroupExtensions, expectedGroupExt)
 		})
 	})
 
 	when("#ReadOrder", func() {
 		it("returns an ordering of buildpacks and an ordering of extensions", func() {
 			h.Mkfile(t, orderTOMLContents, filepath.Join(tmpDir, "order.toml"))
-			foundOrder, foundOrderExt, err := phase.ReadOrder(filepath.Join(tmpDir, "order.toml"))
+			foundOrder, foundOrderExt, err := files.Handler.ReadOrder(filepath.Join(tmpDir, "order.toml"))
 			h.AssertNil(t, err)
 			h.AssertEq(t, foundOrder, expectedOrderBp)
 			h.AssertEq(t, foundOrderExt, expectedOrderExt)
@@ -101,20 +99,20 @@ group = [{id = "D", version = "v1"}]
 
 	when("DefaultConfigHandler", func() {
 		var (
-			configHandler *phase.DefaultConfigHandler
+			configHandler *files.TOMLHandler
 		)
 
 		it.Before(func() {
-			configHandler = phase.NewConfigHandler()
+			configHandler = files.NewHandler()
 		})
 
 		when(".ReadGroup", func() {
 			it("returns a group for buildpacks and a group for extensions", func() {
 				h.Mkfile(t, groupTOMLContents, filepath.Join(tmpDir, "group.toml"))
-				foundGroup, foundGroupExt, err := configHandler.ReadGroup(filepath.Join(tmpDir, "group.toml"))
+				group, err := configHandler.ReadGroup(filepath.Join(tmpDir, "group.toml"))
 				h.AssertNil(t, err)
-				h.AssertEq(t, foundGroup, expectedGroupBp)
-				h.AssertEq(t, foundGroupExt, expectedGroupExt)
+				h.AssertEq(t, group.Group, expectedGroupBp)
+				h.AssertEq(t, group.GroupExtensions, expectedGroupExt)
 			})
 		})
 
diff --git a/phase/testmock/config_handler.go b/phase/testmock/config_handler.go
index a0dc6b1b..166286d9 100644
--- a/phase/testmock/config_handler.go
+++ b/phase/testmock/config_handler.go
@@ -53,13 +53,12 @@ func (mr *MockConfigHandlerMockRecorder) ReadAnalyzed(arg0, arg1 interface{}) *g
 }
 
 // ReadGroup mocks base method.
-func (m *MockConfigHandler) ReadGroup(arg0 string) ([]buildpack.GroupElement, []buildpack.GroupElement, error) {
+func (m *MockConfigHandler) ReadGroup(arg0 string) (buildpack.Group, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "ReadGroup", arg0)
-	ret0, _ := ret[0].([]buildpack.GroupElement)
-	ret1, _ := ret[1].([]buildpack.GroupElement)
-	ret2, _ := ret[2].(error)
-	return ret0, ret1, ret2
+	ret0, _ := ret[0].(buildpack.Group)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
 }
 
 // ReadGroup indicates an expected call of ReadGroup.
diff --git a/platform/files/analyzed_test.go b/platform/files/analyzed_test.go
index 51169536..59f5521c 100644
--- a/platform/files/analyzed_test.go
+++ b/platform/files/analyzed_test.go
@@ -6,7 +6,7 @@ import (
 
 	"github.com/sclevine/spec"
 
-	"github.com/buildpacks/lifecycle/internal/encoding"
+	"github.com/buildpacks/lifecycle/cmd"
 	"github.com/buildpacks/lifecycle/platform/files"
 	h "github.com/buildpacks/lifecycle/testhelpers"
 )
@@ -29,8 +29,8 @@ func testAnalyzed(t *testing.T, when spec.G, it spec.S) {
 					RunImage: &files.RunImage{Reference: "some-ref"},
 				}
 				f := h.TempFile(t, "", "")
-				h.AssertNil(t, encoding.WriteTOML(f, amd))
-				amd2, err := files.ReadAnalyzed(f, nil)
+				h.AssertNil(t, files.Handler.WriteAnalyzed(f, &amd, cmd.DefaultLogger))
+				amd2, err := files.Handler.ReadAnalyzed(f, nil)
 				h.AssertNil(t, err)
 				h.AssertEq(t, amd.PreviousImageRef(), amd2.PreviousImageRef())
 				h.AssertEq(t, amd.LayersMetadata, amd2.LayersMetadata)
@@ -48,7 +48,7 @@ func testAnalyzed(t *testing.T, when spec.G, it spec.S) {
 					RunImage: &files.RunImage{Reference: "some-ref"},
 				}
 				f := h.TempFile(t, "", "")
-				h.AssertNil(t, encoding.WriteTOML(f, amd))
+				h.AssertNil(t, files.Handler.WriteAnalyzed(f, &amd, cmd.DefaultLogger))
 				contents, err := os.ReadFile(f)
 				h.AssertNil(t, err)
 				expectedContents := `[image]
@@ -86,8 +86,8 @@ func testAnalyzed(t *testing.T, when spec.G, it spec.S) {
 					BuildImage: &files.ImageIdentifier{Reference: "implementation"},
 				}
 				f := h.TempFile(t, "", "")
-				h.AssertNil(t, encoding.WriteTOML(f, amd))
-				amd2, err := files.ReadAnalyzed(f, nil)
+				h.AssertNil(t, files.Handler.WriteAnalyzed(f, &amd, cmd.DefaultLogger))
+				amd2, err := files.Handler.ReadAnalyzed(f, nil)
 				h.AssertNil(t, err)
 				h.AssertEq(t, amd.PreviousImageRef(), amd2.PreviousImageRef())
 				h.AssertEq(t, amd.LayersMetadata, amd2.LayersMetadata)
diff --git a/testhelpers/binaries.go b/testhelpers/binaries.go
index fbf1a72f..658c237d 100644
--- a/testhelpers/binaries.go
+++ b/testhelpers/binaries.go
@@ -9,7 +9,7 @@ import (
 )
 
 func MakeAndCopyLifecycle(t *testing.T, goos, goarch, destDir string, envs ...string) {
-	buildDir, err := filepath.Abs(filepath.Join("..", "out"))
+	outParentDir, err := filepath.Abs(filepath.Join("..", "out"))
 	AssertNil(t, err)
 
 	cmd := exec.Command("make", fmt.Sprintf("build-%s-%s", goos, goarch)) // #nosec G204
@@ -21,7 +21,7 @@ func MakeAndCopyLifecycle(t *testing.T, goos, goarch, destDir string, envs ...st
 	envs = append(
 		envs,
 		"PWD="+cmd.Dir,
-		"BUILD_DIR="+buildDir,
+		"BUILD_DIR="+outParentDir,
 	)
 	cmd.Env = append(os.Environ(), envs...)
 
@@ -29,7 +29,8 @@ func MakeAndCopyLifecycle(t *testing.T, goos, goarch, destDir string, envs ...st
 	output := Run(t, cmd)
 	t.Log(output)
 
-	copyLifecycle(t, filepath.Join(buildDir, fmt.Sprintf("%s-%s", goos, goarch), "lifecycle"), destDir)
+	outDir := filepath.Join(outParentDir, fmt.Sprintf("%s-%s", goos, goarch), "lifecycle")
+	copyLifecycle(t, outDir, destDir)
 }
 
 func copyLifecycle(t *testing.T, src, dst string) {