-- SPDX-FileCopyrightText: 2026 Alexandra de Wit
--
-- SPDX-License-Identifier: MIT

{- | Artifact names and URL-component encoding shared by ecosystem routers.
"Ecluse.Core.Text" owns the component gate also used by outbound metadata.
-}
module Ecluse.Core.Server.Path (
    -- * The artifact name
    Filename,
    mkFilename,
    unFilename,

    -- * Component safety
    isSafeComponent,
    encodeComponent,
) where

import Network.HTTP.Types.URI (urlEncode)

import Ecluse.Core.Text (isSafeComponent)

{- | An artifact's on-the-wire file name, verbatim and safe to interpolate: it cleared
'isSafeComponent'. The upstream path uses this exact name, never one rebuilt from the version.
-}
newtype Filename = Filename Text
    deriving stock (Filename -> Filename -> Bool
(Filename -> Filename -> Bool)
-> (Filename -> Filename -> Bool) -> Eq Filename
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Filename -> Filename -> Bool
== :: Filename -> Filename -> Bool
$c/= :: Filename -> Filename -> Bool
/= :: Filename -> Filename -> Bool
Eq, Int -> Filename -> ShowS
[Filename] -> ShowS
Filename -> String
(Int -> Filename -> ShowS)
-> (Filename -> String) -> ([Filename] -> ShowS) -> Show Filename
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Filename -> ShowS
showsPrec :: Int -> Filename -> ShowS
$cshow :: Filename -> String
show :: Filename -> String
$cshowList :: [Filename] -> ShowS
showList :: [Filename] -> ShowS
Show)

{- | Read a filename from untrusted text, 'Nothing' when it is not a safe path component. Every
boundary that admits one (a route capture, a queue payload) parses through this single gate.
-}
mkFilename :: Text -> Maybe Filename
mkFilename :: Text -> Maybe Filename
mkFilename Text
raw
    | Text -> Bool
isSafeComponent Text
raw = Filename -> Maybe Filename
forall a. a -> Maybe a
Just (Text -> Filename
Filename Text
raw)
    | Bool
otherwise = Maybe Filename
forall a. Maybe a
Nothing

-- | The verbatim name, for interpolation into an upstream URL through 'encodeComponent'.
unFilename :: Filename -> Text
unFilename :: Filename -> Text
unFilename (Filename Text
name) = Text
name

{- | Encode a decoded component using only RFC 3986 unreserved bytes verbatim.
Encoding is not idempotent: a literal percent sign becomes @%25@.
-}
encodeComponent :: Text -> Text
-- 'urlEncode' in query-string mode (True), not the path mode http-types recommends: path mode
-- passes ':@&=+$,' through unencoded, which a component must not carry.
encodeComponent :: Text -> Text
encodeComponent = ByteString -> Text
forall a b. ConvertUtf8 a b => b -> a
decodeUtf8 (ByteString -> Text) -> (Text -> ByteString) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Bool -> ByteString -> ByteString
urlEncode Bool
True (ByteString -> ByteString)
-> (Text -> ByteString) -> Text -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> ByteString
forall a b. ConvertUtf8 a b => a -> b
encodeUtf8