Skip to content

fix(profile): keep legacy profile names usable and expand values consistently - #25

Merged
hectorvent merged 2 commits into
mainfrom
fix/profile-edges
Sep 15, 2026
Merged

hectorvent merged 2 commits into
mainfrom
fix/profile-edges

Conversation

@hectorvent

Copy link
Copy Markdown
Contributor

Summary

Two edges of the 0.2.2 profile work (#23), both surfaced by an oversight pass over what landed since 0.2.1. One of them was raised by Greptile as a P1 inline on #23 and merged un-adjudicated.

Profile names that earlier versions accepted became unreachable

The traversal guard added in #23 was an allow-list over the whole character set ([A-Za-z0-9._-]+). That also rejects names 0.2.1 created happily, since profileFile() resolved the raw name with no validation at all. Verified against the released 0.2.2 jar:

$ floci config profile list
Profiles:
  team alpha  http://localhost:4566          # still listed

$ floci config profile show "team alpha"     # exit 1
$ floci config show --profile "team alpha"   # exit 2
$ floci config profile delete "team alpha"   # exit 1  <- cannot even remove it
Error: Invalid profile name 'team alpha'. Use only letters, digits, '.', '_' and '-'.

Blast radius is every name containing a space, +, @ or parens.

The guard is now a deny-list on what actually traverses (., .., path separators), and the guarantee is carried by resolveInProfilesDir, which resolves the file and asserts its parent is the profiles directory. A name that is illegal on the running platform but legal elsewhere, a colon on Windows, now reports a clean message instead of InvalidPathException.

Profile values expanded on one path and not the others

Interpolation of ${env:HOME} is picocli's, applied when it fills an option default. So start --profile p expanded it, while restart --profile p and config show --profile p read the Profile bean directly and got the literal text. Restart asked docker for a directory named ${env:HOME}/floci-data, which is exactly the silent state loss #23 set out to fix, and the README documents the interpolation as a feature.

Both now resolve through StartCommand.resolvedFor, the same provider and precedence a real start uses. That also removes the duplicated field mapping in RestartCommand, so ProfileDefaults is once again the only place a profile field maps to a flag.

Restart also resolves the profile before stopping the container, so a profile that fails to resolve no longer leaves the container stopped.

Verified end to end, real Docker

$ floci start --profile interp --detach --pull never
$ docker inspect floci-interp-test --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}'
/Users/hectorvent/floci-data -> /app/data

$ floci restart --profile interp
$ docker inspect ...                      # same mount, was the literal string before
/Users/hectorvent/floci-data -> /app/data

Plus: a team alpha profile now shows, resolves and deletes; --profile ../escaped still exits 2.

Type of change

  • Bug fix (fix:)
  • New feature (feat:)
  • Breaking change (feat!: or fix!:)
  • Docs / chore

Product trees affected

  • AWS (root tree)
  • GCP (commands/gcp/)
  • Azure (commands/az/)
  • OCI (commands/oci/)

All four through the shared commands/ and config/ classes; no shim changed.

Checklist

  • mvn test passes locally, 140 tests, up from 131
  • New or updated tests added
  • README.md updated, n/a: the documented behaviour is unchanged, this makes it true on every path
  • Native binary verified, native-check --static clean; no Jackson type added, no dependency added, reflect-config.json untouched
  • Commit messages / PR title follow Conventional Commits

Tests

  • ProfileStoreTest: keepsAcceptingNamesEarlierVersionsAllowed over the six shapes 0.2.1 created; aLegacyNameRoundTripsAndCanBeDeleted; everyResolvedFileSitsDirectlyInTheProfilesDirectory pins the real guarantee; the traversal rejections stay.
  • RestartCommandTest.interpolatesProfileValuesExactlyAsStartDoes asserts the expanded mount in the docker argv.
  • ConfigCommandsTest.reportsTheInterpolatedPersistDirThatStartWouldActuallyUse.

Still open from the same pass

Not in this PR: CMD-1 (start --profile p --help shows the product default, not the profile's), CFG-4 (config profile show prints Profile: null where list prints the name), CFG-5 (error messages name a .yaml path when the file is .yml), and the dead no-arg ProfileDefaultValueProvider() constructor. Happy to fold any of them in if you would rather they rode along.

…istently

Two edges of the 0.2.2 profile work, both found by an oversight pass over what
landed since 0.2.1.

Profile names that earlier versions accepted became unreachable. The path
traversal guard added in #23 was an allow-list over the whole character set
([A-Za-z0-9._-]+), which also rejects names 0.2.1 created happily: a space, a
plus, an at sign. Such a profile stayed visible in 'config profile list' while
show, --profile and delete all refused it, so it could not even be removed
through the CLI.

The guard is now a deny-list on what actually traverses ('.', '..', and path
separators), and the guarantee is carried by resolving the file and asserting
its parent is the profiles directory. A name that is illegal on the running
platform but legal elsewhere, such as a colon on Windows, now reports a clean
message instead of InvalidPathException.

Profile values were expanded on one path and not the others. Interpolation of
${env:HOME} and friends is picocli's, applied when it fills an option default,
so 'start --profile p' expanded it while 'restart --profile p' and
'config show --profile p' read the Profile bean directly and got the literal
text. Restart therefore asked docker for a directory named '${env:HOME}/...',
which is the silent state loss #23 set out to fix.

Both now resolve through StartCommand.resolvedFor, the same provider and the
same precedence a real start invocation uses, so one profile cannot mean two
different directories. That also removes the duplicated field mapping in
RestartCommand: ProfileDefaults is the only place a profile field maps to a
flag again.

Restart now resolves the profile before stopping the container, so a profile
that fails to resolve no longer leaves the container stopped.
@greptile-apps

greptile-apps Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR keeps profiles created by older versions usable while still blocking paths that escape the profiles directory. It also makes restart and config show resolve profile values through the same interpolation path as start.

  • Allows legacy names with spaces and other special characters to be read, used, and deleted.
  • Checks the resolved profile file path instead of restricting the full character set.
  • Shares profile expansion between start, restart, and config show.
  • Resolves restart settings before stopping the container and adds coverage for both fixes.

Confidence Score: 5/5

The PR appears safe to merge.

Legacy profile names stay inside the profile folder, and profile values now use one expanded snapshot where needed. The earlier ConfigShowCommand thread was manually resolved without explanation, and the current code also fixes it.

Important Files Changed

Filename Overview
src/main/java/io/floci/cli/config/ProfileStore.java ProfileStore accepts legacy Unix names while keeping each profile file directly inside the profile folder.
src/main/java/io/floci/cli/config/ProfileDefaultValueProvider.java ProfileDefaultValueProvider exposes the profile snapshot it cached during parsing.
src/main/java/io/floci/cli/commands/StartCommand.java StartCommand.resolvedFor applies profiles through the same defaults and value expansion as a normal start.
src/main/java/io/floci/cli/commands/config/ConfigShowCommand.java ConfigShowCommand gets declared keys and expanded values from one profile snapshot.
src/main/java/io/floci/cli/commands/RestartCommand.java RestartCommand resolves start settings before stopping the container and reuses those expanded values.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI
    participant Store as ProfileStore
    participant Provider as ProfileDefaultValueProvider
    participant Start as StartCommand
    participant Docker

    User->>CLI: restart or config show --profile name
    CLI->>Provider: resolve profile defaults
    Provider->>Store: read profile once
    Store-->>Provider: Profile snapshot
    Provider-->>Start: expanded option defaults
    alt restart
        CLI->>Docker: stop container
        CLI->>Start: call cached command
        Start->>Docker: start with expanded values
    else config show
        CLI->>Provider: read cached snapshot
        CLI-->>User: show declared expanded values
    end
Loading

Reviews (2): Last reviewed commit: "fix(profile): stop denying backslash, an..." | Re-trigger Greptile

Comment thread src/main/java/io/floci/cli/config/ProfileStore.java Outdated
Comment thread src/main/java/io/floci/cli/commands/config/ConfigShowCommand.java Outdated
…nfig show

Two P2s from the review on this PR, both correct.

Backslash is a separator on Windows and an ordinary file-name character on
Unix. Denying it outright orphaned a 'team\alpha' profile that 0.2.1 could
create and that list() still returns, which is the same bug class this PR
exists to fix, left half fixed. The character check is now forward slash only;
resolveInProfilesDir already rejects a real Windows traversal on the parent
assertion, so the guarantee is unchanged.

config show read the profile twice: key presence from one read, values from a
second through resolvedFor. A concurrent edit could print half of one version
next to half of another. ProfileDefaultValueProvider already memoizes the
profile it resolved, so exposing that memo gives both from one snapshot, and
resolvedFor gained a provider overload to pass it through. If the profile has
gone since the outer parse, the command now fails loudly instead of printing a
mixed row.

RestartCommand has the same shape between the outer parse and resolvedFor.
Closing it means making the outer provider reachable from commands, which is
wider than this fix; documented in place rather than left silent.
@hectorvent
hectorvent merged commit 7343a53 into main Sep 15, 2026
4 checks passed
@hectorvent

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in version 0.2.3 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant