nova-cache-0.7.0.0: src/NovaCache/SafeName.hs
-- | Windows-unsafe name categories, shared by the store-key allowlist
-- ('NovaCache.Store.sanitizePath') and the NAR entry-name guard in
-- "NovaCache.NAR": names Windows resolves to something other than an
-- ordinary file of that exact spelling. Both guards reject the same
-- categories from one definition, so they cannot drift apart.
--
-- The predicates take raw bytes, the form NAR entry names have. Every
-- category here is ASCII-structural, and UTF-8 lead and continuation
-- bytes are all @>= 0x80@, so byte-level matching is exact - inside
-- valid UTF-8 and inside names that decode as nothing at all. Text
-- callers encode with 'Data.Text.Encoding.encodeUtf8' first.
module NovaCache.SafeName
( isReservedDeviceName,
hasTrailingDotOrSpace,
)
where
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS8
import Data.Char (isAsciiUpper, toLower)
-- | Is the name a Windows reserved device (@con@, @prn@, @aux@, @nul@,
-- @com1@-@com9@, @lpt1@-@lpt9@)? Matched case-insensitively on the portion
-- before the first dot, since @nul.txt@ also opens the device. Enforced on
-- every platform so a Windows-hosted consumer is safe too.
--
-- Device matching is ASCII case-insensitive, so only @A@-@Z@ fold; any
-- other byte passes through and can never match the reserved set.
isReservedDeviceName :: ByteString -> Bool
isReservedDeviceName name =
BS8.map asciiLower (BS8.takeWhile (/= '.') name) `elem` reservedNames
where
asciiLower c = if isAsciiUpper c then toLower c else c
reservedNames =
["con", "prn", "aux", "nul"]
++ [device <> digit | device <- ["com", "lpt"], digit <- digits]
digits = [BS8.pack (show n) | n <- [1 .. 9 :: Int]]
-- | Does the name end with a dot or a space? NTFS strips both at
-- create time, so the on-disk name silently diverges from the requested
-- one and the materialized tree no longer matches what named it.
hasTrailingDotOrSpace :: ByteString -> Bool
hasTrailingDotOrSpace name = case BS8.unsnoc name of
Just (_, end) -> end == '.' || end == ' '
Nothing -> False