Inspection.hs (6574B)
1 {-# LANGUAGE BangPatterns #-} 2 -- Read-only evidence, independent of evaluators and registry transport. 3 module Module.Inspection 4 ( Inspection(..), inspectModule, inspectModuleWithBudget, inspectionStatus, compareGraphs, safeText 5 ) where 6 7 import ContentStore.Object 8 import ContentStore.Alias (ObjectRef(..)) 9 import ContentStore.Filesystem (objectPath) 10 import ContentStore.Bundle (indexedTreeDomain) 11 import Module.Manifest 12 import qualified Wire as W 13 import Control.Exception (IOException, catch, bracket) 14 import Control.Monad (foldM) 15 import qualified Data.ByteString as BS 16 import Data.Text (Text) 17 import qualified Data.Text as T 18 import Data.Text.Encoding (encodeUtf8) 19 import qualified Data.Map.Strict as M 20 import qualified Data.IntMap.Strict as IM 21 import qualified Data.Foldable as F 22 import Data.List (foldl') 23 import Numeric (showHex) 24 import System.IO (openBinaryFile, IOMode(ReadMode), hClose) 25 import System.Posix.Files (getSymbolicLinkStatus, isRegularFile) 26 import System.IO.Error (isDoesNotExistError) 27 28 data Inspection = Inspection 29 { inspectedManifest :: Either Text ModuleManifest 30 , inspectedExports :: M.Map Text (Either Text W.Bundle) 31 } 32 33 -- Escape all non-ASCII/control bytes and '%' in display fields, including errors. 34 safeText :: Text -> Text 35 safeText = T.pack . concatMap escape . BS.unpack . encodeUtf8 36 where 37 escape b | b >= 32 && b <= 126 && b /= 37 = [toEnum (fromIntegral b)] 38 | otherwise = '%' : (if b < 16 then "0" else "") ++ showHex b "" 39 40 readEvidence :: StorePath -> Domain -> Int -> Text -> IO (Either Text BS.ByteString) 41 readEvidence store domain limit pin = case validateObjectHash pin of 42 Left err -> pure (Left ("invalid: " <> T.pack err)) 43 Right () -> (do 44 status <- getSymbolicLinkStatus (objectPath store pin) 45 if not (isRegularFile status) then pure (Left "unavailable: expected regular CAS file") else 46 bracket (openBinaryFile (objectPath store pin) ReadMode) hClose $ \h -> do 47 bytes <- BS.hGet h (limit + 1) 48 pure $ if BS.length bytes > limit then Left "unavailable: object exceeds remaining module inspection byte budget" 49 else case verifyObjectBytes domain pin bytes of 50 Left err -> Left ("invalid: " <> T.pack err) 51 Right () -> Right bytes) 52 `catch` (\(e :: IOException) -> pure (Left (if isDoesNotExistError e then "missing" else "unavailable: " <> T.pack (show e)))) 53 54 inspectModule :: StorePath -> Text -> IO Inspection 55 inspectModule = inspectModuleWithBudget (64 * 1024 * 1024) 56 57 -- Explicit budget is also used by focused tests; the CLI uses the default. 58 inspectModuleWithBudget :: Int -> StorePath -> Text -> IO Inspection 59 inspectModuleWithBudget budget store pin 60 | budget < 0 || budget == maxBound = pure (Inspection (Left "unavailable: invalid inspection budget") M.empty) 61 | otherwise = inspectWithBudget budget store pin 62 63 inspectWithBudget :: Int -> StorePath -> Text -> IO Inspection 64 inspectWithBudget budget store pin = do 65 bytes <- readEvidence store manifestDomain 65536 pin 66 let manifest = bytes >>= \payload -> case decodeManifest payload of 67 Left err -> Left ("invalid: " <> T.pack err) 68 Right m | not (null (moduleManifestReferences m)) || null (moduleManifestExports m) -> Left "unsupported: expected a closed nonempty package module" 69 | otherwise -> Right m 70 exports <- case manifest of 71 Left _ -> pure M.empty 72 Right m -> do 73 (_,_,found) <- foldM inspect (budget, M.empty, M.empty) (moduleManifestExports m) 74 pure found 75 pure (Inspection manifest exports) 76 where 77 inspect (!remaining,!cache,!found) ex = do 78 let ref = moduleExportObject ex 79 cached evidence remainingBytes nextCache = pure (remainingBytes,nextCache,M.insert (moduleExportName ex) evidence found) 80 if objectRefKind ref /= unDomain indexedTreeDomain || moduleExportAbi ex /= "arboricx.abi.tree.v1" 81 then cached (Left "unsupported: expected indexed tree with tree ABI") remaining cache 82 else case M.lookup ref cache of 83 Just evidence -> cached evidence remaining cache 84 Nothing -> do 85 bytes <- readEvidence store indexedTreeDomain remaining (objectRefHash ref) 86 let evidence = bytes >>= \payload -> case decodeGraph payload of 87 Left err -> Left ("invalid: " <> T.pack err) 88 Right graph -> Right graph 89 -- Only retained authenticated bytes count toward graph memory. 90 remainingBytes = either (const remaining) (\payload -> remaining - BS.length payload) bytes 91 evidence `seq` cached evidence remainingBytes (M.insert ref evidence cache) 92 decodeGraph payload = do 93 graph <- W.decodeBundle payload 94 W.verifyBundle graph 95 case (W.bundleRoots graph, W.manifestExports (W.bundleManifest graph)) of 96 ([root],[ex]) | W.exportName ex == "root" && W.exportRoot ex == root 97 && W.exportKind ex == "term" && W.exportAbi ex == "arboricx.abi.tree.v1" -> Right graph 98 _ -> Left "indexed tree requires one root named root with the tree ABI" 99 100 inspectionStatus :: Inspection -> Text 101 inspectionStatus i = case inspectedManifest i of 102 Left err -> err 103 Right _ -> if all good (M.elems (inspectedExports i)) then "verified" else "incomplete (see exports)" 104 where good (Right _) = True; good _ = False 105 106 -- Collision-free hash-consing, not probabilistic subtree hashes or recursive T 107 -- equality. Intern child-ID tuples in ONE table shared by both graphs. Memory is 108 -- proportional to stored records; no logical unfolding, evaluation or CAS writes. 109 compareGraphs :: W.Bundle -> W.Bundle -> Either String Bool 110 compareGraphs left right = do 111 W.verifyBundle left 112 W.verifyBundle right 113 a <- single left; b <- single right 114 let (!table,!l) = intern M.empty left 115 (_,!r) = intern table right 116 pure (l IM.! a == r IM.! b) 117 where 118 single graph = case W.bundleRoots graph of 119 [root] -> Right (fromIntegral root) 120 _ -> Left "structural comparison requires one root" 121 intern initial graph = foldl' step (initial,IM.empty) (zip [0..] (F.toList (W.bundleNodes graph))) 122 step (!table,!indices) (index,node) = 123 let child n = indices IM.! fromIntegral n 124 key = case node of 125 W.BNLeaf -> (0 :: Int,0,0) 126 W.BNStem c -> (1,child c,0) 127 W.BNFork l r -> (2,child l,child r) 128 in case M.lookup key table of 129 Just ident -> (table,IM.insert index ident indices) 130 Nothing -> let ident = M.size table 131 in (M.insert key ident table,IM.insert index ident indices)