Validate portable workspace identifiers

This commit is contained in:
2026-08-10 17:35:52 +00:00
parent 0b40cf8026
commit 1dccf5f140
31 changed files with 490 additions and 48 deletions

View File

@@ -7,6 +7,7 @@ import (
"strings"
"gitea.maximumdirect.net/eric/narratio/internal/config"
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
)
// SessionPaths contains canonical local paths for one session work directory.
@@ -63,11 +64,15 @@ func SessionPreviousArtifactsDirForCampaign(rootDir, campaign, sessionID string)
return filepath.Join(SessionPreviousDirForCampaign(rootDir, campaign, sessionID), config.PathArtifactsDirSegment)
}
// SessionPreviousArtifactPathForCampaign returns a path under previous/artifacts for one artifact.
func SessionPreviousArtifactPathForCampaign(rootDir, campaign, sessionID, artifactRelativePath string) string {
return filepath.Join(
// SessionPreviousArtifactPathForCampaign returns a confined path under
// previous/artifacts for one artifact.
func SessionPreviousArtifactPathForCampaign(rootDir, campaign, sessionID, artifactRelativePath string) (string, error) {
if err := ValidateSessionIdentity(campaign, sessionID); err != nil {
return "", err
}
return pathsafe.JoinSlashRelativeUnderRoot(
SessionPreviousArtifactsDirForCampaign(rootDir, campaign, sessionID),
filepath.FromSlash(previousArtifactCacheRelativePath(artifactRelativePath)),
previousArtifactCacheRelativePath(artifactRelativePath),
)
}
@@ -190,9 +195,34 @@ func SessionPreviousArtifactsDir(paths SessionPaths) string {
return paths.PreviousArtifactsDir
}
// SessionPreviousArtifactPath returns a path under previous/artifacts for already-resolved session paths.
func SessionPreviousArtifactPath(paths SessionPaths, artifactRelativePath string) string {
return filepath.Join(paths.PreviousArtifactsDir, filepath.FromSlash(previousArtifactCacheRelativePath(artifactRelativePath)))
// SessionPreviousArtifactPath returns a confined path under previous/artifacts
// for already-resolved session paths.
func SessionPreviousArtifactPath(paths SessionPaths, artifactRelativePath string) (string, error) {
if err := ValidateSessionIdentity(paths.CampaignID, paths.SessionID); err != nil {
return "", err
}
return pathsafe.JoinSlashRelativeUnderRoot(paths.PreviousArtifactsDir, previousArtifactCacheRelativePath(artifactRelativePath))
}
// ValidateSessionIdentity validates the opaque components used in workspace
// and object-store session paths.
func ValidateSessionIdentity(campaign, sessionID string) error {
if err := pathsafe.ValidateOpaqueSegment(campaign); err != nil {
return fmt.Errorf("campaign identity: %w", err)
}
if err := pathsafe.ValidateOpaqueSegment(sessionID); err != nil {
return fmt.Errorf("session identity: %w", err)
}
return nil
}
// ValidateRunIdentity validates the opaque component used below a session's
// run directory and object-store prefix.
func ValidateRunIdentity(runID string) error {
if err := pathsafe.ValidateOpaqueSegment(runID); err != nil {
return fmt.Errorf("run identity: %w", err)
}
return nil
}
func previousArtifactCacheRelativePath(artifactRelativePath string) string {