module Ecluse.Core.Registry.Npm.Metadata (
fetchNpmManifest,
fetchNpmVersion,
projectNpmManifest,
projectNpmVersion,
) where
import Data.Aeson (Value, eitherDecodeStrict, parseJSON)
import Data.Aeson.Types (parseMaybe)
import Data.Time (UTCTime)
import UnliftIO.Exception (handle)
import Ecluse.Core.Ecosystem (Ecosystem (Npm))
import Ecluse.Core.Package (
PackageDetails,
PackageInfo,
PackageName,
renderPackageName,
)
import Ecluse.Core.Registry (RegistryResponse (responseBody))
import Ecluse.Core.Registry.Metadata (
Manifest (Manifest, manifestDigest, manifestInfo, manifestRaw),
MetadataError (MetadataBoundExceeded, MetadataNameMismatch, MetadataUndecodable),
digestOf,
)
import Ecluse.Core.Registry.Npm (
NpmClientConfig (npmBaseUrl, npmLimits),
ResponseBoundExceeded (ResponseBoundExceeded),
fetchMetadataForm,
)
import Ecluse.Core.Registry.Npm.Project (
Projection (NameMismatch, Projected),
enforceTarballScheme,
enforceTarballSchemeDetails,
parsePackageInfoFromValue,
projectName,
projectVersionEntry,
)
import Ecluse.Core.Registry.Npm.Request (
MetadataForm (Full),
noValidators,
)
import Ecluse.Core.Registry.Npm.SelectiveDecode (
SelectedVersion (svName, svTime, svVersion, svVersionCount),
SelectiveError (SelectiveTooDeeplyNested, SelectiveUndecodable),
selectVersionFromPackument,
)
import Ecluse.Core.Security (
LimitError (TooDeeplyNested, TooManyVersions),
Limits,
checkNestingDepth,
checkVersionCount,
maxNestingDepth,
maxVersionCount,
)
import Ecluse.Core.Telemetry.Span (TracingPort (spanMetadataDecode, spanMetadataFetch))
import Ecluse.Core.Version (Version, mkVersion, renderVersion)
fetchNpmManifest :: TracingPort -> NpmClientConfig -> PackageName -> IO (Either MetadataError Manifest)
fetchNpmManifest :: TracingPort
-> NpmClientConfig
-> PackageName
-> IO (Either MetadataError Manifest)
fetchNpmManifest TracingPort
tracing NpmClientConfig
config PackageName
name =
(ResponseBoundExceeded -> IO (Either MetadataError Manifest))
-> IO (Either MetadataError Manifest)
-> IO (Either MetadataError Manifest)
forall (m :: * -> *) e a.
(MonadUnliftIO m, Exception e) =>
(e -> m a) -> m a -> m a
handle (\(ResponseBoundExceeded LimitError
err) -> Either MetadataError Manifest -> IO (Either MetadataError Manifest)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (MetadataError -> Either MetadataError Manifest
forall a b. a -> Either a b
Left (LimitError -> MetadataError
MetadataBoundExceeded LimitError
err))) (IO (Either MetadataError Manifest)
-> IO (Either MetadataError Manifest))
-> IO (Either MetadataError Manifest)
-> IO (Either MetadataError Manifest)
forall a b. (a -> b) -> a -> b
$ do
response <- TracingPort -> forall a. PackageName -> IO a -> IO a
spanMetadataFetch TracingPort
tracing PackageName
name (IO RegistryResponse -> IO RegistryResponse)
-> IO RegistryResponse -> IO RegistryResponse
forall a b. (a -> b) -> a -> b
$ NpmClientConfig
-> MetadataForm -> Validators -> PackageName -> IO RegistryResponse
fetchMetadataForm NpmClientConfig
config MetadataForm
Full Validators
noValidators PackageName
name
let body = RegistryResponse -> ByteString
responseBody RegistryResponse
response
spanMetadataDecode tracing name $
pure
( manifestOf (digestOf body) . first (enforceTarballScheme (npmBaseUrl config))
<$> projectNpmManifest (npmLimits config) name body
)
where
manifestOf :: ContentDigest -> (PackageInfo, Value) -> Manifest
manifestOf ContentDigest
digest (PackageInfo
info, Value
raw) = Manifest{manifestInfo :: PackageInfo
manifestInfo = PackageInfo
info, manifestRaw :: Value
manifestRaw = Value
raw, manifestDigest :: ContentDigest
manifestDigest = ContentDigest
digest}
projectNpmManifest :: Limits -> PackageName -> ByteString -> Either MetadataError (PackageInfo, Value)
projectNpmManifest :: Limits
-> PackageName
-> ByteString
-> Either MetadataError (PackageInfo, Value)
projectNpmManifest Limits
limits PackageName
name ByteString
body = do
value <- (String -> MetadataError)
-> Either String Value -> Either MetadataError Value
forall a b c. (a -> b) -> Either a c -> Either b c
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (MetadataError -> String -> MetadataError
forall a b. a -> b -> a
const MetadataError
MetadataUndecodable) (ByteString -> Either String Value
forall a. FromJSON a => ByteString -> Either String a
eitherDecodeStrict ByteString
body)
bounded <- first MetadataBoundExceeded (checkNestingDepth limits value)
info <- case parsePackageInfoFromValue name bounded of
Left ParseError
_ -> MetadataError -> Either MetadataError PackageInfo
forall a b. a -> Either a b
Left MetadataError
MetadataUndecodable
Right (NameMismatch Text
reported) -> MetadataError -> Either MetadataError PackageInfo
forall a b. a -> Either a b
Left (Text -> MetadataError
MetadataNameMismatch Text
reported)
Right (Projected PackageInfo
projected) -> PackageInfo -> Either MetadataError PackageInfo
forall a b. b -> Either a b
Right PackageInfo
projected
boundedInfo <- first MetadataBoundExceeded (checkVersionCount limits info)
pure (boundedInfo, bounded)
fetchNpmVersion :: TracingPort -> NpmClientConfig -> PackageName -> Version -> IO (Either MetadataError (Maybe PackageDetails))
fetchNpmVersion :: TracingPort
-> NpmClientConfig
-> PackageName
-> Version
-> IO (Either MetadataError (Maybe PackageDetails))
fetchNpmVersion TracingPort
tracing NpmClientConfig
config PackageName
name Version
version =
(ResponseBoundExceeded
-> IO (Either MetadataError (Maybe PackageDetails)))
-> IO (Either MetadataError (Maybe PackageDetails))
-> IO (Either MetadataError (Maybe PackageDetails))
forall (m :: * -> *) e a.
(MonadUnliftIO m, Exception e) =>
(e -> m a) -> m a -> m a
handle (\(ResponseBoundExceeded LimitError
err) -> Either MetadataError (Maybe PackageDetails)
-> IO (Either MetadataError (Maybe PackageDetails))
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (MetadataError -> Either MetadataError (Maybe PackageDetails)
forall a b. a -> Either a b
Left (LimitError -> MetadataError
MetadataBoundExceeded LimitError
err))) (IO (Either MetadataError (Maybe PackageDetails))
-> IO (Either MetadataError (Maybe PackageDetails)))
-> IO (Either MetadataError (Maybe PackageDetails))
-> IO (Either MetadataError (Maybe PackageDetails))
forall a b. (a -> b) -> a -> b
$ do
response <- TracingPort -> forall a. PackageName -> IO a -> IO a
spanMetadataFetch TracingPort
tracing PackageName
name (IO RegistryResponse -> IO RegistryResponse)
-> IO RegistryResponse -> IO RegistryResponse
forall a b. (a -> b) -> a -> b
$ NpmClientConfig
-> MetadataForm -> Validators -> PackageName -> IO RegistryResponse
fetchMetadataForm NpmClientConfig
config MetadataForm
Full Validators
noValidators PackageName
name
spanMetadataDecode tracing name $
pure ((>>= enforceTarballSchemeDetails (npmBaseUrl config)) <$> projectNpmVersion (npmLimits config) name version (responseBody response))
projectNpmVersion :: Limits -> PackageName -> Version -> ByteString -> Either MetadataError (Maybe PackageDetails)
projectNpmVersion :: Limits
-> PackageName
-> Version
-> ByteString
-> Either MetadataError (Maybe PackageDetails)
projectNpmVersion Limits
limits PackageName
name Version
version ByteString
body = do
selected <- (SelectiveError -> MetadataError)
-> Either SelectiveError SelectedVersion
-> Either MetadataError SelectedVersion
forall a b c. (a -> b) -> Either a c -> Either b c
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (Limits -> SelectiveError -> MetadataError
selectiveError Limits
limits) (Int
-> Version -> ByteString -> Either SelectiveError SelectedVersion
selectVersionFromPackument (Limits -> Int
maxNestingDepth Limits
limits) Version
version ByteString
body)
reported <- validateReportedName (svName selected)
when (reported /= name) (Left (MetadataNameMismatch (renderPackageName reported)))
when
(svVersionCount selected > maxVersionCount limits)
(Left (MetadataBoundExceeded (TooManyVersions (svVersionCount selected) (maxVersionCount limits))))
publishedAt <- parsePublishTime (svTime selected)
pure (svVersion selected >>= projectVersionEntry name (mkVersion Npm (renderVersion version)) publishedAt)
validateReportedName :: Maybe Value -> Either MetadataError PackageName
validateReportedName :: Maybe Value -> Either MetadataError PackageName
validateReportedName = \case
Maybe Value
Nothing -> MetadataError -> Either MetadataError PackageName
forall a b. a -> Either a b
Left MetadataError
MetadataUndecodable
Just Value
nameValue -> case (Value -> Parser Text) -> Value -> Maybe Text
forall a b. (a -> Parser b) -> a -> Maybe b
parseMaybe Value -> Parser Text
forall a. FromJSON a => Value -> Parser a
parseJSON Value
nameValue of
Maybe Text
Nothing -> MetadataError -> Either MetadataError PackageName
forall a b. a -> Either a b
Left MetadataError
MetadataUndecodable
Just Text
raw -> (ParseError -> MetadataError)
-> Either ParseError PackageName
-> Either MetadataError PackageName
forall a b c. (a -> b) -> Either a c -> Either b c
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (MetadataError -> ParseError -> MetadataError
forall a b. a -> b -> a
const MetadataError
MetadataUndecodable) (Text -> Either ParseError PackageName
projectName Text
raw)
parsePublishTime :: Maybe Value -> Either MetadataError (Maybe UTCTime)
parsePublishTime :: Maybe Value -> Either MetadataError (Maybe UTCTime)
parsePublishTime = \case
Maybe Value
Nothing -> Maybe UTCTime -> Either MetadataError (Maybe UTCTime)
forall a b. b -> Either a b
Right Maybe UTCTime
forall a. Maybe a
Nothing
Just Value
timeValue -> Maybe UTCTime -> Either MetadataError (Maybe UTCTime)
forall a b. b -> Either a b
Right ((Value -> Parser UTCTime) -> Value -> Maybe UTCTime
forall a b. (a -> Parser b) -> a -> Maybe b
parseMaybe Value -> Parser UTCTime
forall a. FromJSON a => Value -> Parser a
parseJSON Value
timeValue)
selectiveError :: Limits -> SelectiveError -> MetadataError
selectiveError :: Limits -> SelectiveError -> MetadataError
selectiveError Limits
limits = \case
SelectiveError
SelectiveUndecodable -> MetadataError
MetadataUndecodable
SelectiveError
SelectiveTooDeeplyNested -> LimitError -> MetadataError
MetadataBoundExceeded (Int -> LimitError
TooDeeplyNested (Limits -> Int
maxNestingDepth Limits
limits))