Skip to content

Commit ef3bb12

Browse files
committed
Optimize jni
1 parent 53f81e6 commit ef3bb12

33 files changed

Lines changed: 936 additions & 253 deletions

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vespera/src/multipart.rs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
//! - [`TryFromMultipartWithState<S>`] — Trait for parsing a full multipart request
1414
//! - [`TryFromFieldWithState<S>`] — Trait for parsing a single multipart field
1515
16-
use std::{borrow::Cow, fmt};
16+
use std::{
17+
borrow::Cow,
18+
fmt,
19+
sync::atomic::{AtomicUsize, Ordering},
20+
};
1721

1822
use axum::extract::multipart::{Field, MultipartError, MultipartRejection};
1923
use axum::extract::{FromRequest, Request};
@@ -319,8 +323,28 @@ pub struct FieldMetadata {
319323
pub file_name: Option<String>,
320324
/// The MIME content type of the field.
321325
pub content_type: Option<String>,
322-
/// All HTTP headers associated with this multipart part.
323-
pub headers: axum::http::HeaderMap,
326+
/// Full HTTP headers associated with this multipart part, when explicitly captured.
327+
///
328+
/// Vespera's built-in parsers only need `name`, `file_name`, and `content_type`,
329+
/// so the default `FieldData<T>` path no longer clones the whole `HeaderMap` for
330+
/// every field. Use [`FieldMetadata::with_headers`] when constructing metadata
331+
/// manually and the complete part header map is part of your API contract.
332+
pub headers: Option<axum::http::HeaderMap>,
333+
}
334+
335+
impl FieldMetadata {
336+
/// Return the captured full multipart part headers, if they were collected.
337+
#[must_use]
338+
pub const fn headers(&self) -> Option<&axum::http::HeaderMap> {
339+
self.headers.as_ref()
340+
}
341+
342+
/// Attach a full header snapshot to existing metadata.
343+
#[must_use]
344+
pub fn with_headers(mut self, headers: axum::http::HeaderMap) -> Self {
345+
self.headers = Some(headers);
346+
self
347+
}
324348
}
325349

326350
impl From<&Field<'_>> for FieldMetadata {
@@ -329,7 +353,7 @@ impl From<&Field<'_>> for FieldMetadata {
329353
name: field.name().map(String::from),
330354
file_name: field.file_name().map(String::from),
331355
content_type: field.content_type().map(String::from),
332-
headers: field.headers().clone(),
356+
headers: None,
333357
}
334358
}
335359
}
@@ -531,9 +555,32 @@ const DEFAULT_STRING_FIELD_LIMIT_BYTES: usize = 1024 * 1024; // 1 MiB
531555

532556
/// Default streaming cap for an **unannotated** `NamedTempFile` multipart field.
533557
///
558+
/// The cap is intentionally larger than text fields: unannotated temp-file uploads
559+
/// are real file uploads, but still need a denial-of-service guard by default.
534560
/// Explicit `#[form_data(limit = "unlimited")]` continues to opt out by passing
535-
/// `usize::MAX` through the derive-generated parser.
536-
const DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES: usize = 1024 * 1024; // 1 MiB
561+
/// `usize::MAX` through the derive-generated parser. Applications can tune the
562+
/// process-wide default before handling requests with
563+
/// [`set_default_temp_file_field_limit_bytes`].
564+
pub const DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES: usize = 16 * 1024 * 1024; // 16 MiB
565+
566+
static DEFAULT_TEMP_FILE_FIELD_LIMIT: AtomicUsize =
567+
AtomicUsize::new(DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES);
568+
569+
/// Return the current process-wide default cap for unannotated `NamedTempFile` fields.
570+
#[must_use]
571+
pub fn default_temp_file_field_limit_bytes() -> usize {
572+
DEFAULT_TEMP_FILE_FIELD_LIMIT.load(Ordering::Relaxed)
573+
}
574+
575+
/// Set the process-wide default cap for unannotated `NamedTempFile` fields.
576+
///
577+
/// Call this during application startup, before request handling begins. Per-field
578+
/// `#[form_data(limit = "...")]` annotations still take precedence, including the
579+
/// explicit `"unlimited"` opt-out. The previous cap is returned to support tests or
580+
/// embedders that need to restore their process setting.
581+
pub fn set_default_temp_file_field_limit_bytes(limit_bytes: usize) -> usize {
582+
DEFAULT_TEMP_FILE_FIELD_LIMIT.swap(limit_bytes, Ordering::Relaxed)
583+
}
537584

538585
impl<S: Send + Sync> TryFromFieldWithState<S> for String {
539586
async fn try_from_field_with_state(
@@ -684,7 +731,7 @@ impl<S: Send + Sync> TryFromFieldWithState<S> for tempfile::NamedTempFile {
684731
})?;
685732
let mut file = tokio::fs::File::from_std(std_file);
686733

687-
let limit_bytes = limit_bytes.unwrap_or(DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES);
734+
let limit_bytes = limit_bytes.unwrap_or_else(default_temp_file_field_limit_bytes);
688735
let mut total = 0usize;
689736
while let Some(chunk) = field.chunk().await? {
690737
// `saturating_add` (matching `read_field_data`) prevents a

crates/vespera/src/multipart/tests.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,38 @@ fn test_str_to_bool_trims_surrounding_whitespace() {
4444
assert_eq!(str_to_bool(" "), None);
4545
}
4646

47+
#[test]
48+
fn field_metadata_full_headers_are_optional_by_default() {
49+
let metadata = FieldMetadata {
50+
name: Some("file".to_owned()),
51+
file_name: Some("data.bin".to_owned()),
52+
content_type: Some("application/octet-stream".to_owned()),
53+
headers: None,
54+
};
55+
56+
assert!(metadata.headers().is_none());
57+
}
58+
59+
#[test]
60+
fn temp_file_default_limit_is_bounded_and_configurable() {
61+
assert_eq!(
62+
default_temp_file_field_limit_bytes(),
63+
DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES
64+
);
65+
assert_eq!(DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES, 16 * 1024 * 1024);
66+
67+
let previous = set_default_temp_file_field_limit_bytes(2 * 1024 * 1024);
68+
assert_eq!(previous, DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES);
69+
assert_eq!(default_temp_file_field_limit_bytes(), 2 * 1024 * 1024);
70+
71+
let restored = set_default_temp_file_field_limit_bytes(previous);
72+
assert_eq!(restored, 2 * 1024 * 1024);
73+
assert_eq!(
74+
default_temp_file_field_limit_bytes(),
75+
DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES
76+
);
77+
}
78+
4779
// ─── Display tests for all error variants ───────────────────────────
4880

4981
#[test]

crates/vespera/tests/multipart_wire.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use ::std::io::{Read, Seek, SeekFrom};
1818
use ::std::sync::Once;
1919
use ::tokio::runtime::Builder;
2020
use ::vespera::axum::Json;
21+
use ::vespera::multipart::DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES;
2122
use ::vespera::multipart::{FieldData, TypedMultipart};
2223
use ::vespera::tempfile::NamedTempFile;
2324
use ::vespera::{Multipart, Schema, Validated};
@@ -399,7 +400,7 @@ fn named_temp_file_over_default_cap_rejected_413() {
399400
.build()
400401
.expect("tokio runtime");
401402

402-
let payload = vec![b'z'; 1024 * 1024 + 1];
403+
let payload = vec![b'z'; DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES + 1];
403404
let wire = encode_multipart_upload_wire(
404405
"/capped-upload",
405406
"----TempFileCapBoundary",

crates/vespera_core/src/schema.rs

Lines changed: 130 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
//! Schema-related structure definitions
22
3-
use serde::{Deserialize, Serialize, ser::SerializeStruct};
3+
use serde::{
4+
Deserialize, Serialize,
5+
de::MapAccess,
6+
ser::{SerializeSeq, SerializeStruct},
7+
};
48
use std::collections::BTreeMap;
59

610
/// Schema reference or inline schema.
@@ -29,24 +33,51 @@ impl<'de> Deserialize<'de> for SchemaRef {
2933
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3034
where
3135
D: serde::Deserializer<'de>,
36+
{
37+
deserializer.deserialize_map(SchemaRefVisitor)
38+
}
39+
}
40+
41+
struct SchemaRefVisitor;
42+
43+
impl<'de> serde::de::Visitor<'de> for SchemaRefVisitor {
44+
type Value = SchemaRef;
45+
46+
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47+
formatter.write_str("an OpenAPI schema reference or inline schema object")
48+
}
49+
50+
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
51+
where
52+
M: MapAccess<'de>,
3253
{
3354
use serde::de::Error as _;
34-
// OpenAPI is always JSON; buffer the node so a *pure* reference
35-
// can be distinguished from a `$ref` carrying sibling keywords.
36-
let value = serde_json::Value::deserialize(deserializer)?;
37-
// Pure reference: an object whose ONLY key is `$ref` with a string
38-
// value. A `$ref` with any sibling (`nullable`, `description`, …)
39-
// is an inline schema, so the siblings are preserved instead of
40-
// being dropped by the prior untagged `Ref`-first match.
41-
if let serde_json::Value::Object(map) = &value
42-
&& map.len() == 1
43-
&& let Some(serde_json::Value::String(ref_path)) = map.get("$ref")
44-
{
45-
return Ok(Self::Ref(Reference::new(ref_path.clone())));
55+
56+
let mut ref_path = None;
57+
let mut inline = serde_json::Map::new();
58+
while let Some(key) = access.next_key::<String>()? {
59+
let value = access.next_value::<serde_json::Value>()?;
60+
if key == "$ref"
61+
&& ref_path.is_none()
62+
&& inline.is_empty()
63+
&& let serde_json::Value::String(path) = value
64+
{
65+
ref_path = Some(path);
66+
} else {
67+
if let Some(path) = ref_path.take() {
68+
inline.insert("$ref".to_owned(), serde_json::Value::String(path));
69+
}
70+
inline.insert(key, value);
71+
}
72+
}
73+
74+
if let Some(path) = ref_path {
75+
return Ok(SchemaRef::Ref(Reference::new(path)));
4676
}
47-
serde_json::from_value::<Schema>(value)
48-
.map(|schema| Self::Inline(Box::new(schema)))
49-
.map_err(D::Error::custom)
77+
78+
serde_json::from_value::<Schema>(serde_json::Value::Object(inline))
79+
.map(|schema| SchemaRef::Inline(Box::new(schema)))
80+
.map_err(M::Error::custom)
5081
}
5182
}
5283

@@ -286,6 +317,74 @@ impl Serialize for NumberConstraint {
286317
}
287318
}
288319

320+
struct NullableRefSchema<'a> {
321+
ref_path: &'a str,
322+
}
323+
324+
impl Serialize for NullableRefSchema<'_> {
325+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
326+
where
327+
S: serde::Serializer,
328+
{
329+
let mut out = serializer.serialize_struct("Schema", 1)?;
330+
out.serialize_field("$ref", self.ref_path)?;
331+
out.end()
332+
}
333+
}
334+
335+
struct NullSchema;
336+
337+
impl Serialize for NullSchema {
338+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
339+
where
340+
S: serde::Serializer,
341+
{
342+
let mut out = serializer.serialize_struct("Schema", 1)?;
343+
out.serialize_field("type", &SchemaType::Null)?;
344+
out.end()
345+
}
346+
}
347+
348+
struct NullableRefAnyOf<'a> {
349+
ref_path: &'a str,
350+
}
351+
352+
impl Serialize for NullableRefAnyOf<'_> {
353+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
354+
where
355+
S: serde::Serializer,
356+
{
357+
let mut seq = serializer.serialize_seq(Some(2))?;
358+
seq.serialize_element(&NullableRefSchema {
359+
ref_path: self.ref_path,
360+
})?;
361+
seq.serialize_element(&NullSchema)?;
362+
seq.end()
363+
}
364+
}
365+
366+
struct ExamplesWithLegacy<'a> {
367+
example: Option<&'a serde_json::Value>,
368+
examples: &'a [serde_json::Value],
369+
}
370+
371+
impl Serialize for ExamplesWithLegacy<'_> {
372+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
373+
where
374+
S: serde::Serializer,
375+
{
376+
let len = self.examples.len() + usize::from(self.example.is_some());
377+
let mut seq = serializer.serialize_seq(Some(len))?;
378+
if let Some(example) = self.example {
379+
seq.serialize_element(example)?;
380+
}
381+
for example in self.examples {
382+
seq.serialize_element(example)?;
383+
}
384+
seq.end()
385+
}
386+
}
387+
289388
#[derive(Deserialize, Serialize)]
290389
#[serde(untagged)]
291390
enum SchemaTypeWire {
@@ -430,13 +529,7 @@ impl Serialize for Schema {
430529
let mut out = serializer.serialize_struct("Schema", 42)?;
431530
if let Some(ref_path) = &self.ref_path {
432531
if nullable_ref {
433-
out.serialize_field(
434-
"anyOf",
435-
&[
436-
SchemaRef::Ref(Reference::new(ref_path.clone())),
437-
SchemaRef::Inline(Box::new(Self::new(SchemaType::Null))),
438-
],
439-
)?;
532+
out.serialize_field("anyOf", &NullableRefAnyOf { ref_path })?;
440533
} else {
441534
out.serialize_field("$ref", ref_path)?;
442535
}
@@ -463,13 +556,22 @@ impl Serialize for Schema {
463556
}
464557
match (&self.example, &self.examples) {
465558
(Some(example), Some(examples)) => {
466-
let mut combined_examples = Vec::with_capacity(examples.len() + 1);
467-
combined_examples.push(example);
468-
combined_examples.extend(examples);
469-
out.serialize_field("examples", &combined_examples)?;
559+
out.serialize_field(
560+
"examples",
561+
&ExamplesWithLegacy {
562+
example: Some(example),
563+
examples,
564+
},
565+
)?;
470566
}
471567
(Some(example), None) => {
472-
out.serialize_field("examples", &[example])?;
568+
out.serialize_field(
569+
"examples",
570+
&ExamplesWithLegacy {
571+
example: Some(example),
572+
examples: &[],
573+
},
574+
)?;
473575
}
474576
(None, Some(examples)) => {
475577
out.serialize_field("examples", examples)?;

crates/vespera_core/src/schema/tests.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,19 @@ fn schema_level_example_serializes_as_examples_array() {
9898
assert_eq!(value["examples"], serde_json::json!(["abc", "def"]));
9999
}
100100

101+
#[test]
102+
fn schema_level_example_and_examples_serialization_is_byte_identical() {
103+
let schema = Schema {
104+
example: Some(serde_json::json!("abc")),
105+
examples: Some(vec![serde_json::json!("def")]),
106+
..Schema::string()
107+
};
108+
109+
let json = serde_json::to_string(&schema).unwrap();
110+
111+
assert_eq!(json, r#"{"type":"string","examples":["abc","def"]}"#);
112+
}
113+
101114
#[test]
102115
fn schema_level_legacy_example_deserializes_for_round_trip_compatibility() {
103116
let schema: Schema = serde_json::from_value(serde_json::json!({
@@ -272,6 +285,18 @@ fn nullable_reference_emits_anyof_ref_and_null_only() {
272285
);
273286
}
274287

288+
#[test]
289+
fn nullable_reference_serialization_is_byte_identical() {
290+
let schema = Schema::nullable_reference("#/components/schemas/User".to_owned());
291+
292+
let json = serde_json::to_string(&schema).unwrap();
293+
294+
assert_eq!(
295+
json,
296+
r##"{"anyOf":[{"$ref":"#/components/schemas/User"},{"type":"null"}]}"##
297+
);
298+
}
299+
275300
#[test]
276301
fn nullable_primitive_emits_type_array_with_null() {
277302
let schema = Schema {

0 commit comments

Comments
 (0)