1-- SPDX-FileCopyrightText: 2026 Sören Tempel <soeren+git@soeren-tempel.net>2--3-- SPDX-License-Identifier: GPL-3.0-only45-- | This module provides a parser for the @ar(5)@ file format. The6-- implementation provided here is modeled after OpenBSD's7-- [@ar(5)@](https://man.openbsd.org/ar.5) man page. The format has never been8-- standartized thus, incompatibilities between different @ar(5)@ formats. See9-- the HISTORY section in the aforementioned OpenBSD man page for more10-- information. Most implementations are suffciently compatible when not using11-- file names with spaces in them.12--13-- See also <https://marc.info/?t=179121127700004&r=1&w=2>.14module Data.Archive15 ( Object (..),16 Data.Archive.parse,17 qbeArchive,18 )19where2021import Control.Applicative ((<|>))22import Control.Exception (throwIO)23import Control.Monad (replicateM, void)24import Data.Maybe (catMaybes)25import Language.QBE (Program, parse)26import Numeric (readOct)27import Text.ParserCombinators.Parsec28 ( ParseError,29 Parser,30 SourceName,31 anyChar,32 char,33 count,34 digit,35 eof,36 many,37 noneOf,38 octDigit,39 optionMaybe,40 parse,41 skipMany,42 string,43 )4445data Object46 = Object47 { oName :: String,48 oTime :: Int,49 oUser :: Int,50 oGroup :: Int,51 oMode :: Int,52 oSize :: Int,53 oData :: String54 }55 deriving (Show, Eq)5657magicString :: Parser ()58magicString = void $ string "!<arch>\n"5960headerTrailer :: Parser ()61headerTrailer = void $ string "`\n"6263lexme :: Parser a -> Parser a64lexme p = p <* skipMany (char ' ')6566upTo :: Int -> Parser a -> Parser [a]67upTo n p68 | n <= 0 = return []69 | otherwise = catMaybes <$> replicateM n (optionMaybe p)7071------------------------------------------------------------------------7273-- From OpenBSD's ar(5):74--75-- > Only the name field has any provision for overflow. If any file name76-- > is more than 16 characters in length or contains an embedded space,77-- > the string "#1/" followed by the ASCII length of the name is written78-- > in the name field.79--80objName :: Parser (Either Int String)81objName =82 Left <$> (string "#1/" >> read <$> upTo 13 digit)83 <|> Right <$> upTo 16 (noneOf " ")8485objTime :: Parser Int86objTime = read <$> upTo 12 digit8788objUser :: Parser Int89objUser = read <$> upTo 6 digit9091objGroup :: Parser Int92objGroup = objUser9394objMode :: Parser Int95objMode = getOct . readOct <$> upTo 8 octDigit96 where97 getOct [(num, "")] = num98 getOct _ = error "unreachable"99100objSize :: Parser Int101objSize = read <$> upTo 10 digit102103------------------------------------------------------------------------104105-- From OpenBSD's ar(5):106--107-- > Objects in the archive are always an even number of bytes long; files108-- > which are an odd number of bytes long are padded with a newline (‘\n’)109-- > character, although the size in the header does not reflect this.110--111skipPadding :: Int -> Parser ()112skipPadding size113 | odd size = void (char '\n')114 | otherwise = pure ()115116object :: Parser Object117object = do118 name <- lexme objName119 time <- lexme objTime120 oUid <- lexme objUser121 oGid <- lexme objGroup122 mode <- lexme objMode123 size <- lexme objSize124125 headerTrailer126 (name', size') <-127 -- Overflow handling for name, see the 'objName' comment above.128 case name of129 Left ns ->130 if ns > size131 then fail "name length exceeds object length"132 else (,size - ns) <$> count ns anyChar133 Right n -> pure (n, size)134135 body <- count size' anyChar <* skipPadding size'136 pure $137 Object138 { oName = name',139 oTime = time,140 oUser = oUid,141 oGroup = oGid,142 oMode = mode,143 oSize = size,144 oData = body145 }146147------------------------------------------------------------------------148149parse :: SourceName -> String -> Either ParseError [Object]150parse =151 Text.ParserCombinators.Parsec.parse152 ((magicString >> many object) <* eof)153154qbeArchive :: FilePath -> IO [Program]155qbeArchive fp = do156 ar <- readFile fp157 objs <- case Data.Archive.parse fp ar of158 Right x -> pure x159 Left err -> throwIO err160 mapM (\o -> parseFile (oName o) (oData o)) objs161 where162 parseFile name body =163 case Language.QBE.parse name body of164 Right x -> pure x165 Left err -> throwIO err