Manifest.hs (4942B)
1 module Module.Manifest 2 ( ModuleManifest(..) 3 , ModuleReference(..) 4 , ModuleExport(..) 5 , manifestDomain 6 , encodeManifest 7 , decodeManifest 8 , putManifest 9 , getManifest 10 ) where 11 12 import ContentStore.Filesystem (getVerifiedObject, putObject) 13 import ContentStore.Object 14 import ContentStore.Alias (ObjectRef(..)) 15 16 import Data.ByteString (ByteString) 17 import Data.Text (Text) 18 import Data.Text.Encoding (decodeUtf8', encodeUtf8) 19 20 import qualified Data.Text as Text 21 import qualified Data.Set as Set 22 23 -- | Immutable module artifact. Names are export labels inside this manifest; 24 -- content identity is carried by object references and the manifest CAS hash. 25 data ModuleManifest = ModuleManifest 26 { moduleManifestReferences :: [ModuleReference] 27 , moduleManifestExports :: [ModuleExport] 28 } deriving (Eq, Ord, Show) 29 30 -- | Direct content-addressed reference needed to understand, fetch, or audit 31 -- this manifest. The alias is human-facing metadata for diagnostics/workspace 32 -- presentation; the referenced object is the portable identity. These are not 33 -- source-language imports. 34 data ModuleReference = ModuleReference 35 { moduleReferenceAlias :: Text 36 , moduleReferenceRef :: ObjectRef 37 } deriving (Eq, Ord, Show) 38 39 -- | Exported executable artifact. Optional contract terms are ordinary tree 40 -- terms referenced from elsewhere in the store, not a special artifact kind. 41 data ModuleExport = ModuleExport 42 { moduleExportName :: Text 43 , moduleExportObject :: ObjectRef 44 , moduleExportAbi :: Text 45 } deriving (Eq, Ord, Show) 46 47 manifestDomain :: Domain 48 manifestDomain = Domain "arboricx.module-manifest.v1" 49 50 encodeManifest :: ModuleManifest -> ByteString 51 encodeManifest manifest = encodeUtf8 $ Text.unlines $ 52 ["arboricx.module-manifest.v1"] 53 ++ map encodeReference (moduleManifestReferences manifest) 54 ++ map encodeExport (moduleManifestExports manifest) 55 where 56 encodeReference ref = Text.intercalate "\t" 57 [ "reference" 58 , esc (moduleReferenceAlias ref) 59 , esc (objectRefKind $ moduleReferenceRef ref) 60 , esc (objectRefHash $ moduleReferenceRef ref) 61 ] 62 encodeExport ex = Text.intercalate "\t" 63 [ "export" 64 , esc (moduleExportName ex) 65 , esc (objectRefKind $ moduleExportObject ex) 66 , esc (objectRefHash $ moduleExportObject ex) 67 , esc (moduleExportAbi ex) 68 ] 69 70 -- | Parse the canonical manifest encoding. 71 decodeManifest :: ByteString -> Either String ModuleManifest 72 decodeManifest bs = do 73 txt <- either (Left . show) Right (decodeUtf8' bs) 74 case Text.lines txt of 75 [] -> Left "empty module manifest" 76 header : rows 77 | header /= "arboricx.module-manifest.v1" -> Left "unsupported module manifest version" 78 | otherwise -> do 79 manifest <- foldl step (Right (ModuleManifest [] [])) rows 80 let names = map moduleExportName (moduleManifestExports manifest) 81 if Set.size (Set.fromList names) /= length names 82 then Left "duplicate export name" 83 else Right manifest 84 where 85 step acc line = do 86 manifest <- acc 87 case Text.splitOn "\t" line of 88 ["reference", alias, kind, hash] -> do 89 ref <- ModuleReference <$> unesc alias <*> (ObjectRef <$> unesc kind <*> unesc hash) 90 Right manifest { moduleManifestReferences = moduleManifestReferences manifest ++ [ref] } 91 ["export", name, kind, hash, abi] -> do 92 ex <- ModuleExport 93 <$> unesc name 94 <*> (ObjectRef <$> unesc kind <*> unesc hash) 95 <*> unesc abi 96 Right manifest { moduleManifestExports = moduleManifestExports manifest ++ [ex] } 97 _ -> Left $ "invalid module manifest row: " ++ Text.unpack line 98 99 putManifest :: StorePath -> ModuleManifest -> IO ObjectHash 100 putManifest store = putObject store manifestDomain . encodeManifest 101 102 getManifest :: StorePath -> ObjectHash -> IO (Maybe ModuleManifest) 103 getManifest store h = do 104 mBytes <- getVerifiedObject store manifestDomain h 105 case mBytes of 106 Nothing -> return Nothing 107 Just bytes -> case decodeManifest bytes of 108 Left err -> fail $ "invalid module manifest " ++ Text.unpack h ++ ": " ++ err 109 Right manifest -> return (Just manifest) 110 111 esc :: Text -> Text 112 esc = Text.concatMap $ \c -> case c of 113 '%' -> "%25" 114 '\t' -> "%09" 115 '\n' -> "%0A" 116 '\r' -> "%0D" 117 _ -> Text.singleton c 118 119 unesc :: Text -> Either String Text 120 unesc txt = go txt "" 121 where 122 go rest acc = case Text.uncons rest of 123 Nothing -> Right acc 124 Just ('%', xs) -> 125 let (code, tail') = Text.splitAt 2 xs 126 decoded = case code of 127 "25" -> Just "%" 128 "09" -> Just "\t" 129 "0A" -> Just "\n" 130 "0D" -> Just "\r" 131 _ -> Nothing 132 in case decoded of 133 Nothing -> Left $ "invalid percent escape: %" ++ Text.unpack code 134 Just c -> go tail' (acc <> c) 135 Just (c, xs) -> go xs (acc <> Text.singleton c)