diff --git a/uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js b/uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js index 7930514..b0ec4f6 100644 --- a/uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js +++ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js @@ -44,12 +44,13 @@ export default { return 0; }, - // Fetch.fetch(url, method, body) -> packed Uzumibi::Response + // Fetch.fetch(url, method, body, headers) -> packed Uzumibi::Response // Format: u16 status | u16 headers_count | (u16 key_size, key, u16 value_size, value)... | u32 body_size | body uzumibi_cf_fetch: async ( urlPtr, urlSize, methodPtr, methodSize, bodyPtr, bodySize, + headersPtr, headersSize, resultPtr, resultMaxSize ) => { const memory = exports.memory; @@ -64,6 +65,28 @@ export default { fetchOptions.body = body; } + // Unpack request headers: u16 LE count, then (u16 LE key_size, key, u16 LE value_size, value) * count + if (headersSize >= 2) { + const hView = new DataView(memory.buffer, headersPtr, headersSize); + const hCount = hView.getUint16(0, true); + if (hCount > 0) { + const reqHeaders = {}; + let hPos = 2; + for (let i = 0; i < hCount; i++) { + const kLen = hView.getUint16(hPos, true); + hPos += 2; + const k = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, kLen)); + hPos += kLen; + const vLen = hView.getUint16(hPos, true); + hPos += 2; + const v = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, vLen)); + hPos += vLen; + reqHeaders[k] = v; + } + fetchOptions.headers = reqHeaders; + } + } + const response = await fetch(url, fetchOptions); const responseBody = await response.text(); diff --git a/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/src/lib.rs b/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/src/lib.rs index 6ee35d3..afbfe1f 100644 --- a/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/src/lib.rs +++ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/src/lib.rs @@ -44,6 +44,8 @@ unsafe extern "C" { method_size: usize, body_ptr: *const u8, body_size: usize, + headers_ptr: *const u8, + headers_size: usize, result_ptr: *mut u8, result_max_size: usize, ) -> i32; @@ -82,7 +84,7 @@ fn debug_console_log_internal(message: &str) { /// u32 LE body_size /// body bytes #[cfg(feature = "enable-external")] -fn cf_fetch(url: &str, method: &str, body: &str) -> Result, String> { +fn cf_fetch(url: &str, method: &str, body: &str, headers: &[u8]) -> Result, String> { const BUFFER_SIZE: usize = 65536; let mut buffer = vec![0u8; BUFFER_SIZE]; @@ -94,6 +96,8 @@ fn cf_fetch(url: &str, method: &str, body: &str) -> Result, String> { method.len(), body.as_ptr(), body.len(), + headers.as_ptr(), + headers.len(), buffer.as_mut_ptr(), BUFFER_SIZE, ); @@ -181,7 +185,7 @@ fn uzumibi_kernel_debug_console_log( Ok(RObject::nil().to_refcount_assigned()) } -/// Fetch.fetch(url, method="GET", body="") -> Uzumibi::Response +/// Fetch.fetch(url, method="GET", body="", headers={}) -> Uzumibi::Response #[cfg(feature = "enable-external")] fn uzumibi_fetch_class_fetch( vm: &mut VM, @@ -207,13 +211,55 @@ fn uzumibi_fetch_class_fetch( String::new() }; - let packed = cf_fetch(&url, &method, &body) + // Pack request headers from Hash (4th argument) + let packed_headers = if args.len() > 3 { + pack_headers_from_hash(vm, &args[3])? + } else { + vec![0u8; 2] // u16 LE count = 0 + }; + + let packed = cf_fetch(&url, &method, &body, &packed_headers) .map_err(|e| mrubyedge::Error::RuntimeError(format!("Fetch failed: {}", e)))?; // Unpack the packed response into Uzumibi::Response unpack_response_to_robject(vm, &packed) } +/// Pack a mruby Hash into binary format for request headers: +/// u16 LE headers_count +/// (u16 LE key_size, key bytes, u16 LE value_size, value bytes) * count +#[cfg(feature = "enable-external")] +fn pack_headers_from_hash( + vm: &mut VM, + hash_obj: &Rc, +) -> Result, mrubyedge::Error> { + match &hash_obj.as_ref().value { + RValue::Hash(h) => { + let hash = h.borrow(); + let mut buf = Vec::new(); + let count = hash.len() as u16; + buf.extend_from_slice(&count.to_le_bytes()); + for (_, (key_obj, value_obj)) in hash.iter() { + let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?; + let key: String = key.as_ref().try_into()?; + let value = mrb_funcall(vm, value_obj.clone().into(), "to_s", &[])?; + let value: String = value.as_ref().try_into()?; + buf.extend_from_slice(&(key.len() as u16).to_le_bytes()); + buf.extend_from_slice(key.as_bytes()); + buf.extend_from_slice(&(value.len() as u16).to_le_bytes()); + buf.extend_from_slice(value.as_bytes()); + } + Ok(buf) + } + RValue::Nil => { + Ok(vec![0u8; 2]) // u16 LE count = 0 + } + _ => Err(mrubyedge::Error::RuntimeError( + "headers argument must be a Hash".to_string(), + )), + } +} + /// Unpack packed binary response into Uzumibi::Response mruby object #[cfg(feature = "enable-external")] fn unpack_response_to_robject(vm: &mut VM, buf: &[u8]) -> Result, mrubyedge::Error> { diff --git a/uzumibi-cli/templates/cloudflare/__features__/queue/src/index.js b/uzumibi-cli/templates/cloudflare/__features__/queue/src/index.js index a3c60f1..eb41615 100644 --- a/uzumibi-cli/templates/cloudflare/__features__/queue/src/index.js +++ b/uzumibi-cli/templates/cloudflare/__features__/queue/src/index.js @@ -48,11 +48,12 @@ export default { return 0; }, - // Fetch.fetch(url, method, body) -> packed Uzumibi::Response + // Fetch.fetch(url, method, body, headers) -> packed Uzumibi::Response uzumibi_cf_fetch: async ( urlPtr, urlSize, methodPtr, methodSize, bodyPtr, bodySize, + headersPtr, headersSize, resultPtr, resultMaxSize, ) => { const memory = exports.memory; @@ -67,6 +68,28 @@ export default { fetchOptions.body = body; } + // Unpack request headers: u16 LE count, then (u16 LE key_size, key, u16 LE value_size, value) * count + if (headersSize >= 2) { + const hView = new DataView(memory.buffer, headersPtr, headersSize); + const hCount = hView.getUint16(0, true); + if (hCount > 0) { + const reqHeaders = {}; + let hPos = 2; + for (let i = 0; i < hCount; i++) { + const kLen = hView.getUint16(hPos, true); + hPos += 2; + const k = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, kLen)); + hPos += kLen; + const vLen = hView.getUint16(hPos, true); + hPos += 2; + const v = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, vLen)); + hPos += vLen; + reqHeaders[k] = v; + } + fetchOptions.headers = reqHeaders; + } + } + const response = await fetch(url, fetchOptions); const responseBody = await response.text(); diff --git a/uzumibi-cli/templates/cloudflare/__features__/queue/wasm-app/src/lib.rs b/uzumibi-cli/templates/cloudflare/__features__/queue/wasm-app/src/lib.rs index d072d9e..11e7074 100644 --- a/uzumibi-cli/templates/cloudflare/__features__/queue/wasm-app/src/lib.rs +++ b/uzumibi-cli/templates/cloudflare/__features__/queue/wasm-app/src/lib.rs @@ -57,6 +57,8 @@ unsafe extern "C" { method_size: usize, body_ptr: *const u8, body_size: usize, + headers_ptr: *const u8, + headers_size: usize, result_ptr: *mut u8, result_max_size: usize, ) -> i32; @@ -95,7 +97,7 @@ fn debug_console_log_internal(message: &str) { /// u32 LE body_size /// body bytes #[cfg(feature = "enable-external")] -fn cf_fetch(url: &str, method: &str, body: &str) -> Result, String> { +fn cf_fetch(url: &str, method: &str, body: &str, headers: &[u8]) -> Result, String> { const BUFFER_SIZE: usize = 65536; let mut buffer = vec![0u8; BUFFER_SIZE]; @@ -107,6 +109,8 @@ fn cf_fetch(url: &str, method: &str, body: &str) -> Result, String> { method.len(), body.as_ptr(), body.len(), + headers.as_ptr(), + headers.len(), buffer.as_mut_ptr(), BUFFER_SIZE, ); @@ -194,7 +198,7 @@ fn uzumibi_kernel_debug_console_log( Ok(RObject::nil().to_refcount_assigned()) } -/// Fetch.fetch(url, method="GET", body="") -> Uzumibi::Response +/// Fetch.fetch(url, method="GET", body="", headers={}) -> Uzumibi::Response #[cfg(feature = "enable-external")] fn uzumibi_fetch_class_fetch( vm: &mut VM, @@ -220,13 +224,55 @@ fn uzumibi_fetch_class_fetch( String::new() }; - let packed = cf_fetch(&url, &method, &body) + // Pack request headers from Hash (4th argument) + let packed_headers = if args.len() > 3 { + pack_headers_from_hash(vm, &args[3])? + } else { + vec![0u8; 2] // u16 LE count = 0 + }; + + let packed = cf_fetch(&url, &method, &body, &packed_headers) .map_err(|e| mrubyedge::Error::RuntimeError(format!("Fetch failed: {}", e)))?; // Unpack the packed response into Uzumibi::Response unpack_response_to_robject(vm, &packed) } +/// Pack a mruby Hash into binary format for request headers: +/// u16 LE headers_count +/// (u16 LE key_size, key bytes, u16 LE value_size, value bytes) * count +#[cfg(feature = "enable-external")] +fn pack_headers_from_hash( + vm: &mut VM, + hash_obj: &Rc, +) -> Result, mrubyedge::Error> { + match &hash_obj.as_ref().value { + RValue::Hash(h) => { + let hash = h.borrow(); + let mut buf = Vec::new(); + let count = hash.len() as u16; + buf.extend_from_slice(&count.to_le_bytes()); + for (_, (key_obj, value_obj)) in hash.iter() { + let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?; + let key: String = key.as_ref().try_into()?; + let value = mrb_funcall(vm, value_obj.clone().into(), "to_s", &[])?; + let value: String = value.as_ref().try_into()?; + buf.extend_from_slice(&(key.len() as u16).to_le_bytes()); + buf.extend_from_slice(key.as_bytes()); + buf.extend_from_slice(&(value.len() as u16).to_le_bytes()); + buf.extend_from_slice(value.as_bytes()); + } + Ok(buf) + } + RValue::Nil => { + Ok(vec![0u8; 2]) // u16 LE count = 0 + } + _ => Err(mrubyedge::Error::RuntimeError( + "headers argument must be a Hash".to_string(), + )), + } +} + /// Unpack packed binary response into Uzumibi::Response mruby object #[cfg(feature = "enable-external")] fn unpack_response_to_robject(vm: &mut VM, buf: &[u8]) -> Result, mrubyedge::Error> { diff --git a/uzumibi-cli/tests/runn/help.yml b/uzumibi-cli/tests/runn/help.yml index ce583f5..7c57bac 100644 --- a/uzumibi-cli/tests/runn/help.yml +++ b/uzumibi-cli/tests/runn/help.yml @@ -1,7 +1,6 @@ desc: Test uzumibi CLI help command vars: binary: ${UZUMIBI_TEST_BINARY:-../target/release/uzumibi} - version: 0.6.0-rc2 steps: build: desc: Build release binary @@ -28,7 +27,7 @@ steps: version: desc: Show version exec: - command: "{{ vars.binary }} --version | grep {{ vars.version }}" + command: "{{ vars.binary }} --version" test: | current.exit_code == 0 && current.stdout contains "uzumibi" diff --git a/uzumibi-on-cloudflare-spike/src/index.js b/uzumibi-on-cloudflare-spike/src/index.js index eb1e5cb..8551be5 100644 --- a/uzumibi-on-cloudflare-spike/src/index.js +++ b/uzumibi-on-cloudflare-spike/src/index.js @@ -44,12 +44,13 @@ export default { return 0; }, - // Fetch.fetch(url, method, body) -> packed Uzumibi::Response + // Fetch.fetch(url, method, body, headers) -> packed Uzumibi::Response // Format: u16 status | u16 headers_count | (u16 key_size, key, u16 value_size, value)... | u32 body_size | body uzumibi_cf_fetch: async ( urlPtr, urlSize, methodPtr, methodSize, bodyPtr, bodySize, + headersPtr, headersSize, resultPtr, resultMaxSize ) => { const memory = exports.memory; @@ -64,6 +65,28 @@ export default { fetchOptions.body = body; } + // Unpack request headers: u16 LE count, then (u16 LE key_size, key, u16 LE value_size, value) * count + if (headersSize >= 2) { + const hView = new DataView(memory.buffer, headersPtr, headersSize); + const hCount = hView.getUint16(0, true); + if (hCount > 0) { + const reqHeaders = {}; + let hPos = 2; + for (let i = 0; i < hCount; i++) { + const kLen = hView.getUint16(hPos, true); + hPos += 2; + const k = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, kLen)); + hPos += kLen; + const vLen = hView.getUint16(hPos, true); + hPos += 2; + const v = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, vLen)); + hPos += vLen; + reqHeaders[k] = v; + } + fetchOptions.headers = reqHeaders; + } + } + const response = await fetch(url, fetchOptions); const responseBody = await response.text(); diff --git a/uzumibi-on-cloudflare-spike/src/index.queue.js b/uzumibi-on-cloudflare-spike/src/index.queue.js index 6c03e1c..e18fa1f 100644 --- a/uzumibi-on-cloudflare-spike/src/index.queue.js +++ b/uzumibi-on-cloudflare-spike/src/index.queue.js @@ -48,11 +48,12 @@ export default { return 0; }, - // Fetch.fetch(url, method, body) -> packed Uzumibi::Response + // Fetch.fetch(url, method, body, headers) -> packed Uzumibi::Response uzumibi_cf_fetch: async ( urlPtr, urlSize, methodPtr, methodSize, bodyPtr, bodySize, + headersPtr, headersSize, resultPtr, resultMaxSize, ) => { const memory = exports.memory; @@ -67,6 +68,28 @@ export default { fetchOptions.body = body; } + // Unpack request headers: u16 LE count, then (u16 LE key_size, key, u16 LE value_size, value) * count + if (headersSize >= 2) { + const hView = new DataView(memory.buffer, headersPtr, headersSize); + const hCount = hView.getUint16(0, true); + if (hCount > 0) { + const reqHeaders = {}; + let hPos = 2; + for (let i = 0; i < hCount; i++) { + const kLen = hView.getUint16(hPos, true); + hPos += 2; + const k = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, kLen)); + hPos += kLen; + const vLen = hView.getUint16(hPos, true); + hPos += 2; + const v = decoder.decode(new Uint8Array(memory.buffer, headersPtr + hPos, vLen)); + hPos += vLen; + reqHeaders[k] = v; + } + fetchOptions.headers = reqHeaders; + } + } + const response = await fetch(url, fetchOptions); const responseBody = await response.text(); diff --git a/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml b/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml index d8b0554..34eacec 100644 --- a/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml +++ b/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml @@ -17,6 +17,6 @@ uzumibi-art-router = ">= 0.3.1" mruby-compiler2-sys = ">= 0.3.0" [features] -default = [] +default = ["enable-external"] enable-external = [] queue = ["enable-external"] diff --git a/uzumibi-on-cloudflare-spike/wasm-app/src/lib.rs b/uzumibi-on-cloudflare-spike/wasm-app/src/lib.rs index 00c2cf7..dbbdb9a 100644 --- a/uzumibi-on-cloudflare-spike/wasm-app/src/lib.rs +++ b/uzumibi-on-cloudflare-spike/wasm-app/src/lib.rs @@ -57,6 +57,8 @@ unsafe extern "C" { method_size: usize, body_ptr: *const u8, body_size: usize, + headers_ptr: *const u8, + headers_size: usize, result_ptr: *mut u8, result_max_size: usize, ) -> i32; @@ -95,7 +97,7 @@ fn debug_console_log_internal(message: &str) { /// u32 LE body_size /// body bytes #[cfg(feature = "enable-external")] -fn cf_fetch(url: &str, method: &str, body: &str) -> Result, String> { +fn cf_fetch(url: &str, method: &str, body: &str, headers: &[u8]) -> Result, String> { const BUFFER_SIZE: usize = 65536; let mut buffer = vec![0u8; BUFFER_SIZE]; @@ -107,6 +109,8 @@ fn cf_fetch(url: &str, method: &str, body: &str) -> Result, String> { method.len(), body.as_ptr(), body.len(), + headers.as_ptr(), + headers.len(), buffer.as_mut_ptr(), BUFFER_SIZE, ); @@ -194,7 +198,7 @@ fn uzumibi_kernel_debug_console_log( Ok(RObject::nil().to_refcount_assigned()) } -/// Fetch.fetch(url, method="GET", body="") -> Uzumibi::Response +/// Fetch.fetch(url, method="GET", body="", headers={}) -> Uzumibi::Response #[cfg(feature = "enable-external")] fn uzumibi_fetch_class_fetch( vm: &mut VM, @@ -220,13 +224,55 @@ fn uzumibi_fetch_class_fetch( String::new() }; - let packed = cf_fetch(&url, &method, &body) + // Pack request headers from Hash (4th argument) + let packed_headers = if args.len() > 3 { + pack_headers_from_hash(vm, &args[3])? + } else { + vec![0u8; 2] // u16 LE count = 0 + }; + + let packed = cf_fetch(&url, &method, &body, &packed_headers) .map_err(|e| mrubyedge::Error::RuntimeError(format!("Fetch failed: {}", e)))?; // Unpack the packed response into Uzumibi::Response unpack_response_to_robject(vm, &packed) } +/// Pack a mruby Hash into binary format for request headers: +/// u16 LE headers_count +/// (u16 LE key_size, key bytes, u16 LE value_size, value bytes) * count +#[cfg(feature = "enable-external")] +fn pack_headers_from_hash( + vm: &mut VM, + hash_obj: &Rc, +) -> Result, mrubyedge::Error> { + match &hash_obj.as_ref().value { + RValue::Hash(h) => { + let hash = h.borrow(); + let mut buf = Vec::new(); + let count = hash.len() as u16; + buf.extend_from_slice(&count.to_le_bytes()); + for (_, (key_obj, value_obj)) in hash.iter() { + let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?; + let key: String = key.as_ref().try_into()?; + let value = mrb_funcall(vm, value_obj.clone().into(), "to_s", &[])?; + let value: String = value.as_ref().try_into()?; + buf.extend_from_slice(&(key.len() as u16).to_le_bytes()); + buf.extend_from_slice(key.as_bytes()); + buf.extend_from_slice(&(value.len() as u16).to_le_bytes()); + buf.extend_from_slice(value.as_bytes()); + } + Ok(buf) + } + RValue::Nil => { + Ok(vec![0u8; 2]) // u16 LE count = 0 + } + _ => Err(mrubyedge::Error::RuntimeError( + "headers argument must be a Hash".to_string(), + )), + } +} + /// Unpack packed binary response into Uzumibi::Response mruby object #[cfg(feature = "enable-external")] fn unpack_response_to_robject(vm: &mut VM, buf: &[u8]) -> Result, mrubyedge::Error> {