fix: form bug

This commit is contained in:
Orion Kindel 2024-11-23 13:49:36 -06:00
parent fc33a076db
commit 1a1d5526b7
Signed by untrusted user who does not match committer: orion
GPG Key ID: 6D4165AE4C928719
8 changed files with 1558 additions and 831 deletions

23
.spec-results Normal file
View File

@ -0,0 +1,23 @@
[
[
"Form fromRaw",
{
"timestamp": "1732391355864.0",
"success": true
}
],
[
"Form ok",
{
"timestamp": "1732390464524.0",
"success": true
}
],
[
"Form toRaw",
{
"timestamp": "1732391355864.0",
"success": true
}
]
]

2234
spago.lock

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,11 @@ package:
location: location:
githubOwner: 'cakekindel' githubOwner: 'cakekindel'
githubRepo: 'purescript-ezfetch' githubRepo: 'purescript-ezfetch'
test:
main: 'Test.Main'
dependencies:
- spec
- spec-node
dependencies: dependencies:
- aff: ">=7.1.0 <8.0.0" - aff: ">=7.1.0 <8.0.0"
- aff-promise: ">=4.0.0 <5.0.0" - aff-promise: ">=4.0.0 <5.0.0"

View File

@ -1,4 +1,4 @@
/** @type {(_: Record<string, Array<string | Blob>>) => () => FormData} */ /** @type {(_: Record<string, Array<string | File>>) => () => FormData} */
export const unsafeMakeFormData = o => () => { export const unsafeMakeFormData = o => () => {
const form = new FormData() const form = new FormData()
@ -11,20 +11,20 @@ export const unsafeMakeFormData = o => () => {
return form return form
} }
/** @typedef {{filename: string | null, mime: string, buf: ArrayBuffer}} FileRecord */ /** @typedef {{filename: string, mime: string, buf: ArrayBuffer}} FileRecord */
/** @type {(_: FileRecord) => () => Blob} */ /** @type {(_: FileRecord) => () => File} */
export const unsafeMakeBlob = export const unsafeMakeFile =
({ mime, buf }) => ({ mime, buf, filename }) =>
() => () =>
new Blob([buf], { type: mime }) new File([buf], filename, { type: mime })
/** @type {(_: FormData) => () => Promise<Record<string, Array<string | FileRecord>>>} */ /** @type {(_: FormData) => () => Promise<Record<string, Array<string | FileRecord>>>} */
export const unsafeUnmakeFormData = fd => async () => { export const unsafeUnmakeFormData = fd => async () => {
/** @type {Record<string, Array<string | FileRecord>>} */ /** @type {Record<string, Array<string | FileRecord>>} */
const rec = {} const rec = {}
for (const [k, ent_] of fd.entries()) { for (const [k, ent_] of fd.entries()) {
/** @type {File | Blob | string} */ /** @type {File | string} */
const ent = ent_ const ent = ent_
/** @type {string | FileRecord} */ /** @type {string | FileRecord} */
@ -35,8 +35,6 @@ export const unsafeUnmakeFormData = fd => async () => {
buf: await ent.arrayBuffer(), buf: await ent.arrayBuffer(),
mime: ent.type, mime: ent.type,
} }
} else if (ent instanceof Blob) {
append = { filename: null, buf: await ent.arrayBuffer(), mime: ent.type }
} else { } else {
append = ent append = ent
} }
@ -54,3 +52,29 @@ export const unsafeUnmakeFormData = fd => async () => {
return rec return rec
} }
/** @type {(a: ArrayBuffer) => (b: ArrayBuffer) => boolean} */
export const unsafeEqArrayBuffer = a => b => {
try {
if (a.byteLength !== b.byteLength) return false
const ua = new Uint8Array(a)
const ub = new Uint8Array(b)
let pass = true
for (let i = 0; i < a.byteLength; i++) {
pass = pass && ua[i] === ub[i]
}
return pass
} catch {
return false
}
}
/** @type {(a: ArrayBuffer) => string} */
export const unsafeShowArrayBuffer = a => {
try {
return Buffer.from(a).toString('base64url')
} catch (e) {
return e instanceof Error ? e.toString() : ''
}
}

View File

@ -10,12 +10,12 @@ import Data.ArrayBuffer.Types (ArrayBuffer)
import Data.Either (hush) import Data.Either (hush)
import Data.FoldableWithIndex (foldlWithIndex) import Data.FoldableWithIndex (foldlWithIndex)
import Data.Generic.Rep (class Generic) import Data.Generic.Rep (class Generic)
import Data.MIME (MIME)
import Data.MIME as MIME
import Data.Map (Map) import Data.Map (Map)
import Data.Map as Map import Data.Map as Map
import Data.Maybe (Maybe(..)) import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype, unwrap, wrap) import Data.Newtype (class Newtype, unwrap, wrap)
import Data.Nullable (Nullable)
import Data.Nullable as Nullable
import Data.Show.Generic (genericShow) import Data.Show.Generic (genericShow)
import Data.Traversable (for) import Data.Traversable (for)
import Effect (Effect) import Effect (Effect)
@ -25,16 +25,16 @@ import Effect.Exception (error)
import Foreign (Foreign, unsafeReadTagged, unsafeToForeign) import Foreign (Foreign, unsafeReadTagged, unsafeToForeign)
import Foreign.Object (Object) import Foreign.Object (Object)
import Foreign.Object as Object import Foreign.Object as Object
import Data.MIME (MIME)
import Data.MIME as MIME
import Simple.JSON (readImpl, unsafeStringify) import Simple.JSON (readImpl, unsafeStringify)
import Unsafe.Coerce (unsafeCoerce) import Unsafe.Coerce (unsafeCoerce)
import Web.File.Blob (Blob) import Web.File.File (File)
type FileRecord = { filename :: Nullable String, mime :: String, buf :: ArrayBuffer } type FileRecord = { filename :: String, mime :: String, buf :: ArrayBuffer }
foreign import data RawFormData :: Type foreign import data RawFormData :: Type
foreign import unsafeMakeBlob :: FileRecord -> Effect Blob foreign import unsafeShowArrayBuffer :: ArrayBuffer -> String
foreign import unsafeEqArrayBuffer :: ArrayBuffer -> ArrayBuffer -> Boolean
foreign import unsafeMakeFile :: FileRecord -> Effect File
foreign import unsafeMakeFormData :: Object (Array Foreign) -> Effect RawFormData foreign import unsafeMakeFormData :: Object (Array Foreign) -> Effect RawFormData
foreign import unsafeUnmakeFormData :: RawFormData -> Effect (Promise (Object (Array Foreign))) foreign import unsafeUnmakeFormData :: RawFormData -> Effect (Promise (Object (Array Foreign)))
@ -50,16 +50,16 @@ derive newtype instance Ord Filename
data Value data Value
= ValueString String = ValueString String
| ValueFile (Maybe Filename) ArrayBuffer MIME | ValueFile Filename ArrayBuffer MIME
valueForeign :: Value -> Effect Foreign valueForeign :: Value -> Effect Foreign
valueForeign (ValueString s) = pure $ unsafeToForeign s valueForeign (ValueString s) = pure $ unsafeToForeign s
valueForeign (ValueFile filename buf mime) = unsafeToForeign <$> unsafeMakeBlob { filename: Nullable.toNullable $ unwrap <$> filename, buf, mime: MIME.toString mime } valueForeign (ValueFile filename buf mime) = unsafeToForeign <$> unsafeMakeFile { filename: unwrap filename, buf, mime: MIME.toString mime }
valueFromForeign :: Foreign -> Effect Value valueFromForeign :: Foreign -> Effect Value
valueFromForeign f = do valueFromForeign f = do
let let
file :: Maybe { filename :: Nullable String, buf :: Foreign, mime :: String } file :: Maybe { filename :: String, buf :: Foreign, mime :: String }
file = hush $ runExcept $ readImpl f file = hush $ runExcept $ readImpl f
string = hush $ runExcept $ unsafeReadTagged "String" f string = hush $ runExcept $ unsafeReadTagged "String" f
@ -69,7 +69,7 @@ valueFromForeign f = do
buf' :: ArrayBuffer buf' :: ArrayBuffer
buf' = unsafeCoerce buf buf' = unsafeCoerce buf
in in
pure $ ValueFile (wrap <$> Nullable.toMaybe filename) buf' (MIME.fromString mime) pure $ ValueFile (wrap filename) buf' (MIME.fromString mime)
Nothing -> do Nothing -> do
s <- liftMaybe (error $ "invalid form value " <> unsafeStringify f) string s <- liftMaybe (error $ "invalid form value " <> unsafeStringify f) string
pure $ ValueString s pure $ ValueString s
@ -77,12 +77,21 @@ valueFromForeign f = do
derive instance Generic Value _ derive instance Generic Value _
instance Show Value where instance Show Value where
show (ValueString s) = "(ValueString " <> show s <> ")" show (ValueString s) = "(ValueString " <> show s <> ")"
show (ValueFile filename _ mime) = "(ValueFile (" <> show filename <> ") <ArrayBuffer> (" <> show mime <> "))" show (ValueFile filename buf mime) = "(ValueFile (" <> show filename <> ") (ArrayBuffer " <> unsafeShowArrayBuffer buf <> ") (" <> show mime <> "))"
instance Eq Value where
eq (ValueString a) (ValueString b) = a == b
eq (ValueFile namea bufa mimea) (ValueFile nameb bufb mimeb)
| namea /= nameb = false
| mimea /= mimeb = false
| otherwise = unsafeEqArrayBuffer bufa bufb
eq _ _ = false
newtype Form = Form (Map String (Array Value)) newtype Form = Form (Map String (Array Value))
derive instance Newtype Form _ derive instance Newtype Form _
derive newtype instance Show Form derive newtype instance Show Form
derive newtype instance Eq Form
fromRaw :: forall m. MonadAff m => RawFormData -> m Form fromRaw :: forall m. MonadAff m => RawFormData -> m Form
fromRaw f = do fromRaw f = do
@ -101,3 +110,4 @@ toRawFormData =
pure $ Object.insert k vs' o' pure $ Object.insert k vs' o'
in in
liftEffect <<< flip bind unsafeMakeFormData <<< foldlWithIndex collect (pure Object.empty) <<< unwrap liftEffect <<< flip bind unsafeMakeFormData <<< foldlWithIndex collect (pure Object.empty) <<< unwrap

View File

@ -0,0 +1,7 @@
export const dummyForm = () => {
const form = new FormData()
form.set('foo', 'bar')
const hi = Buffer.from('hello, world!', 'utf8')
form.set('baz', new Blob([hi.buffer.slice(hi.byteOffset, hi.byteOffset + hi.byteLength)], {type: 'text/plain'}), 'foo.txt')
return form
}

View File

@ -0,0 +1,32 @@
module Test.Effect.Aff.HTTP.Form where
import Prelude
import Data.MIME as MIME
import Data.Map as Map
import Data.Tuple.Nested ((/\))
import Effect (Effect)
import Effect.Aff.HTTP.Form (Form(..), RawFormData)
import Effect.Aff.HTTP.Form as Form
import Effect.Class (liftEffect)
import Node.Buffer as Buffer
import Node.Encoding (Encoding(..))
import Test.Spec (Spec, describe, it)
import Test.Spec.Assertions (shouldEqual)
foreign import dummyForm :: Effect RawFormData
spec :: Spec Unit
spec = describe "Form" do
it "fromRaw" do
dummy <- liftEffect dummyForm
f <- Form.fromRaw dummy
buf <- Buffer.fromString "hello, world!" UTF8 >>= Buffer.toArrayBuffer # liftEffect
f `shouldEqual` Form (Map.fromFoldable [ "foo" /\ [ Form.ValueString "bar" ], "baz" /\ [ Form.ValueFile (Form.Filename "foo.txt") buf MIME.Txt ] ])
it "toRaw" do
expect <- liftEffect dummyForm >>= Form.fromRaw
buf <- Buffer.fromString "hello, world!" UTF8 >>= Buffer.toArrayBuffer # liftEffect
let
f = Form (Map.fromFoldable [ "foo" /\ [ Form.ValueString "bar" ], "baz" /\ [ Form.ValueFile (Form.Filename "foo.txt") buf MIME.Txt ] ])
actual <- Form.toRawFormData f >>= Form.fromRaw
expect `shouldEqual` actual

12
test/Test.Main.purs Normal file
View File

@ -0,0 +1,12 @@
module Test.Main where
import Prelude
import Effect (Effect)
import Test.Spec.Reporter (specReporter)
import Test.Spec.Runner.Node (runSpecAndExitProcess)
import Test.Effect.Aff.HTTP.Form as Test.Form
main :: Effect Unit
main = runSpecAndExitProcess [ specReporter ] do
Test.Form.spec