first
This commit is contained in:
commit
a3b253ce0c
8 changed files with 315 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
dist
|
||||
dist-newstyle
|
49
ips.cabal
Normal file
49
ips.cabal
Normal file
|
@ -0,0 +1,49 @@
|
|||
cabal-version: 2.2
|
||||
name: ips
|
||||
version: 0.1.0
|
||||
synopsis: applies ips patches. remember those?
|
||||
license: WTFPL
|
||||
|
||||
author: rhiannon morris <rhi@rhiannon.website>
|
||||
maintainer: rhiannon morris <rhi@rhiannon.website>
|
||||
|
||||
common deps
|
||||
default-language: Haskell2010
|
||||
default-extensions:
|
||||
BlockArguments,
|
||||
DerivingStrategies,
|
||||
DuplicateRecordFields,
|
||||
FlexibleContexts,
|
||||
GeneralizedNewtypeDeriving,
|
||||
NamedFieldPuns,
|
||||
OverloadedStrings,
|
||||
TupleSections
|
||||
build-depends:
|
||||
base ^>= 4.14.1.0,
|
||||
bytestring ^>= 0.11.1.0,
|
||||
vector ^>= 0.12.2.0,
|
||||
attoparsec ^>= 0.13.2.5,
|
||||
primitive ^>= 0.7.1.0
|
||||
ghc-options:
|
||||
-Wall
|
||||
|
||||
common exe
|
||||
build-depends:
|
||||
ips
|
||||
ghc-options:
|
||||
-threaded -rtsopts -with-rtsopts=-N
|
||||
|
||||
library
|
||||
import: deps
|
||||
hs-source-dirs: src
|
||||
exposed-modules: IPS
|
||||
other-modules:
|
||||
IPS.Word24,
|
||||
IPS.Types,
|
||||
IPS.Parse,
|
||||
IPS.Apply
|
||||
|
||||
executable ips
|
||||
import: deps, exe
|
||||
hs-source-dirs: main
|
||||
main-is: ips.hs
|
27
main/ips.hs
Normal file
27
main/ips.hs
Normal file
|
@ -0,0 +1,27 @@
|
|||
module Main (main) where
|
||||
|
||||
import IPS (Bytes, makeBytes)
|
||||
import qualified IPS
|
||||
import System.Environment
|
||||
import System.IO
|
||||
import qualified Data.ByteString as ByteString
|
||||
import qualified Data.Vector.Storable as Vector
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
args <- getArgs
|
||||
case args of
|
||||
[inf, ipsf, outf] -> do
|
||||
buf <- readBytes inf
|
||||
ips <- either error id <$> IPS.read ipsf
|
||||
writeBytes outf $ IPS.apply ips buf
|
||||
_ -> error "usage: $0 <in> <ips> <out>"
|
||||
|
||||
readBytes :: FilePath -> IO Bytes
|
||||
readBytes f = makeBytes <$> ByteString.readFile f
|
||||
|
||||
writeBytes :: FilePath -> Bytes -> IO ()
|
||||
writeBytes f buf =
|
||||
withFile f WriteMode \h ->
|
||||
Vector.unsafeWith buf \ptr ->
|
||||
hPutBuf h ptr (Vector.length buf)
|
5
src/IPS.hs
Normal file
5
src/IPS.hs
Normal file
|
@ -0,0 +1,5 @@
|
|||
module IPS (module IPS) where
|
||||
|
||||
import IPS.Types as IPS
|
||||
import IPS.Parse as IPS
|
||||
import IPS.Apply as IPS
|
62
src/IPS/Apply.hs
Normal file
62
src/IPS/Apply.hs
Normal file
|
@ -0,0 +1,62 @@
|
|||
module IPS.Apply (apply, applyM, unsafeApplyM) where
|
||||
|
||||
import IPS.Types
|
||||
import Data.Vector.Storable.Mutable (MVector)
|
||||
import qualified Data.Vector.Storable.Mutable as MVector
|
||||
import qualified Data.Vector.Storable as Vector
|
||||
import Control.Monad.ST
|
||||
import Control.Monad.Primitive (PrimMonad (PrimState))
|
||||
import Data.Foldable
|
||||
|
||||
|
||||
type BytesM m = MVector (PrimState m) Word8
|
||||
|
||||
-- explodes if the patch tries to write outside the existing space
|
||||
unsafeApplyM :: PrimMonad m => Patch -> BytesM m -> m ()
|
||||
unsafeApplyM p buf =
|
||||
for_ p \Chunk {offset, body} ->
|
||||
case body of
|
||||
Normal bs -> do
|
||||
let offset' = fromIntegral offset
|
||||
let sz = Vector.length bs
|
||||
let dst = MVector.slice offset' sz buf
|
||||
Vector.unsafeCopy dst bs
|
||||
RLE {size, value} -> do
|
||||
let offset' = fromIntegral offset
|
||||
let sz = fromIntegral size
|
||||
let dst = MVector.slice offset' sz buf
|
||||
MVector.set dst value
|
||||
|
||||
-- size of patch output
|
||||
-- (patches can write past the end of the input file)
|
||||
outSize :: Patch -> Int
|
||||
outSize = foldl' (\m c -> max m (end c)) 0 where
|
||||
end (Chunk {offset, body}) = case body of
|
||||
Normal bs -> offset' + Vector.length bs
|
||||
RLE {size} -> offset' + fromIntegral size
|
||||
where offset' = fromIntegral offset
|
||||
|
||||
-- applies in place if it fits, otherwise allocates a new buffer for it
|
||||
applyM :: PrimMonad m => Patch -> BytesM m -> m (Maybe (BytesM m))
|
||||
applyM p buf = do
|
||||
let size = outSize p
|
||||
(buf', realloc) <-
|
||||
if MVector.length buf >= size then
|
||||
pure (buf, False)
|
||||
else do
|
||||
newBuf <- MVector.new size
|
||||
let dst = MVector.slice 0 (MVector.length buf) newBuf
|
||||
MVector.copy dst buf
|
||||
pure (newBuf, True)
|
||||
unsafeApplyM p buf'
|
||||
pure $ if realloc then Just buf' else Nothing
|
||||
|
||||
-- copies an immutable vector (resizing if needed) and patches it
|
||||
apply :: Patch -> Bytes -> Bytes
|
||||
apply p inp = runST do
|
||||
let size = outSize p `max` Vector.length inp
|
||||
buf <- MVector.new size
|
||||
let dst = MVector.slice 0 (Vector.length inp) buf
|
||||
Vector.copy dst inp
|
||||
unsafeApplyM p buf
|
||||
Vector.unsafeFreeze buf
|
51
src/IPS/Parse.hs
Normal file
51
src/IPS/Parse.hs
Normal file
|
@ -0,0 +1,51 @@
|
|||
module IPS.Parse (parser, read) where
|
||||
|
||||
import Prelude hiding (read)
|
||||
import IPS.Types
|
||||
import Data.Attoparsec.ByteString (Parser)
|
||||
import qualified Data.Attoparsec.ByteString as Parse
|
||||
import Data.Bits
|
||||
import Data.Bifunctor
|
||||
import qualified Data.ByteString as ByteString
|
||||
import Data.Functor
|
||||
import qualified Data.Vector.Generic as Vector
|
||||
import Control.Applicative
|
||||
|
||||
|
||||
read :: FilePath -> IO (Either String Patch)
|
||||
read f = Parse.parseOnly parser <$> ByteString.readFile f
|
||||
|
||||
|
||||
parser :: Parser Patch
|
||||
parser = Parse.string "PATCH" *> parseBody
|
||||
|
||||
parseBody :: Parser Patch
|
||||
parseBody = uncurry Vector.fromListN <$> go where
|
||||
go = eof <|> chunk
|
||||
eof = Parse.string "EOF" $> (0, [])
|
||||
chunk = liftA2 cons parseChunk go
|
||||
where cons c = bimap (+ 1) (c :)
|
||||
|
||||
parseChunk :: Parser Chunk
|
||||
parseChunk = do
|
||||
offset <- parseWord24
|
||||
size₀ <- parseWord16
|
||||
body <-
|
||||
if size₀ == 0 then
|
||||
liftA2 RLE parseWord16 Parse.anyWord8
|
||||
else
|
||||
Normal . makeBytes <$> Parse.take (fromIntegral size₀)
|
||||
pure $ Chunk {offset, body}
|
||||
|
||||
parseWord16 :: Parser Word16
|
||||
parseWord16 = parseWordBE 2
|
||||
|
||||
parseWord24 :: Parser Word24
|
||||
parseWord24 = parseWordBE 3
|
||||
|
||||
parseWordBE :: (Integral a, Bits a) => Word -> Parser a
|
||||
parseWordBE = go 0 where
|
||||
go acc 0 = pure acc
|
||||
go acc i = do
|
||||
b <- fromIntegral <$> Parse.anyWord8
|
||||
go ((acc `shiftL` 8) .|. b) (i - 1)
|
35
src/IPS/Types.hs
Normal file
35
src/IPS/Types.hs
Normal file
|
@ -0,0 +1,35 @@
|
|||
module IPS.Types
|
||||
(Patch, Chunk (..), ChunkBody (..),
|
||||
Word8, Word16, Word24,
|
||||
Bytes, makeBytes)
|
||||
where
|
||||
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as ByteString
|
||||
import Data.Vector (Vector)
|
||||
import qualified Data.Vector.Storable as S
|
||||
import Data.Word
|
||||
import IPS.Word24
|
||||
|
||||
|
||||
type Patch = Vector Chunk
|
||||
|
||||
data Chunk =
|
||||
Chunk {
|
||||
offset :: {-# UNPACK #-} !Word24,
|
||||
body :: !ChunkBody
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
data ChunkBody =
|
||||
Normal {-# UNPACK #-} !Bytes
|
||||
| RLE {
|
||||
size :: {-# UNPACK #-} !Word16,
|
||||
value :: {-# UNPACK #-} !Word8
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
type Bytes = S.Vector Word8
|
||||
|
||||
makeBytes :: ByteString -> Bytes
|
||||
makeBytes bs = S.generate (ByteString.length bs) (ByteString.index bs)
|
84
src/IPS/Word24.hs
Normal file
84
src/IPS/Word24.hs
Normal file
|
@ -0,0 +1,84 @@
|
|||
module IPS.Word24 (Word24) where
|
||||
|
||||
import Data.Word
|
||||
import Data.Maybe
|
||||
import Data.Bits
|
||||
|
||||
|
||||
newtype Word24 = W24 Word32
|
||||
deriving newtype (Eq, Ord, Show)
|
||||
|
||||
maxW24I :: Int
|
||||
maxW24I = 0xFFFFFF
|
||||
|
||||
maxW24 :: Word32
|
||||
maxW24 = 0xFFFFFF
|
||||
|
||||
mkW24 :: Word32 -> Word24
|
||||
mkW24 x = W24 $ x .&. maxW24
|
||||
|
||||
instance Read Word24 where
|
||||
readsPrec d str = mapMaybe isW24 $ readsPrec d str where
|
||||
isW24 (x, r) | x <= maxW24 = Just (W24 x, r)
|
||||
| otherwise = Nothing
|
||||
|
||||
instance Bounded Word24 where
|
||||
minBound = W24 0
|
||||
maxBound = W24 maxW24
|
||||
|
||||
instance Enum Word24 where
|
||||
succ (W24 x) | x >= maxW24 = error "succ @Word24: maxBound"
|
||||
| otherwise = W24 $ succ x
|
||||
|
||||
pred (W24 x) | x <= 0 = error "pred @Word24: 0"
|
||||
| otherwise = W24 $ pred x
|
||||
|
||||
toEnum x | x >= 0 && x <= maxW24I = W24 $ fromIntegral x
|
||||
| otherwise = error "toEnum @Word24: out of range"
|
||||
|
||||
fromEnum (W24 x) = fromEnum x
|
||||
|
||||
clamp1 :: (Word32 -> Word32) -> (Word24 -> Word24)
|
||||
clamp1 f (W24 x) = mkW24 $ f x
|
||||
clamp2 :: (Word32 -> Word32 -> Word32) -> (Word24 -> Word24 -> Word24)
|
||||
clamp2 (·) (W24 x) (W24 y) = mkW24 $ x · y
|
||||
|
||||
instance Num Word24 where
|
||||
(+) = clamp2 (+)
|
||||
(*) = clamp2 (*)
|
||||
(-) = clamp2 (-)
|
||||
|
||||
abs = id
|
||||
signum x = if x == 0 then 0 else 1
|
||||
fromInteger x = mkW24 $ fromInteger x
|
||||
|
||||
instance Real Word24 where
|
||||
toRational (W24 x) = toRational x
|
||||
|
||||
instance Integral Word24 where
|
||||
W24 x `quotRem` W24 y = let (q, r) = x `quotRem` y in (mkW24 q, mkW24 r)
|
||||
toInteger (W24 x) = toInteger x
|
||||
|
||||
instance Bits Word24 where
|
||||
(.&.) = clamp2 (.&.)
|
||||
(.|.) = clamp2 (.|.)
|
||||
xor = clamp2 xor
|
||||
|
||||
complement = clamp1 complement
|
||||
|
||||
shift (W24 x) i = mkW24 $ shift x i
|
||||
|
||||
rotate x 0 = x
|
||||
rotate (W24 x) i = mkW24 $ (x `shiftL` i) .|. (x `shiftR` (24 - i))
|
||||
|
||||
bitSize = finiteBitSize
|
||||
bitSizeMaybe = Just . finiteBitSize
|
||||
isSigned _ = False
|
||||
|
||||
testBit = testBitDefault
|
||||
bit = bitDefault
|
||||
|
||||
popCount (W24 x) = popCount x
|
||||
|
||||
instance FiniteBits Word24 where
|
||||
finiteBitSize _ = 24
|
Loading…
Reference in a new issue