diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be910fd38..b7e82ab8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: strategy: fail-fast: false matrix: - rust: [beta, stable, 1.45.0, 1.40.0, 1.38.0, 1.36.0] + rust: [beta, stable, 1.53.0, 1.46.0, 1.45.0, 1.40.0, 1.38.0, 1.36.0] os: [ubuntu] include: - rust: stable @@ -44,14 +44,16 @@ jobs: with: toolchain: ${{matrix.rust}} - run: cargo check - - run: cargo check --features preserve_order - if: matrix.rust != '1.36.0' - - run: cargo check --manifest-path tests/crate/Cargo.toml --no-default-features --features alloc,preserve_order - if: matrix.rust != '1.36.0' - run: cargo check --features float_roundtrip - run: cargo check --features arbitrary_precision - run: cargo check --features raw_value - run: cargo check --features unbounded_depth + - run: cargo check --manifest-path tests/crate/Cargo.toml --no-default-features --features alloc + - run: cargo check --manifest-path tests/crate/Cargo.toml --no-default-features --features alloc,raw_value + - run: cargo check --features preserve_order + if: matrix.rust != '1.45.0' && matrix.rust != '1.40.0' && matrix.rust != '1.38.0' && matrix.rust != '1.36.0' + - run: cargo check --manifest-path tests/crate/Cargo.toml --no-default-features --features alloc,preserve_order + if: matrix.rust != '1.45.0' && matrix.rust != '1.40.0' && matrix.rust != '1.38.0' && matrix.rust != '1.36.0' - name: Build without std run: | rustup target add aarch64-unknown-none @@ -62,25 +64,18 @@ jobs: --features alloc if: matrix.rust == 'stable' && matrix.os == 'ubuntu' - nostd: - name: Rust 1.36.0 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: dtolnay/rust-toolchain@1.36.0 - - run: cargo check --manifest-path tests/crate/Cargo.toml --no-default-features --features alloc - miri: name: Miri runs-on: ubuntu-latest + env: + MIRIFLAGS: "-Zmiri-tag-raw-pointers" steps: - uses: actions/checkout@v2 - uses: dtolnay/rust-toolchain@nightly with: components: miri - run: cargo miri test - env: - MIRIFLAGS: "-Zmiri-tag-raw-pointers" + - run: cargo miri test --features preserve_order,float_roundtrip,arbitrary_precision,raw_value clippy: name: Clippy diff --git a/Cargo.toml b/Cargo.toml index 96034225e..00098582c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "serde_json" -version = "1.0.74" # remember to update html_root_url +version = "1.0.78" # remember to update html_root_url authors = ["Erick Tryzelaar ", "David Tolnay "] license = "MIT OR Apache-2.0" description = "A JSON serialization file format" @@ -9,7 +9,6 @@ documentation = "https://docs.serde.rs/serde_json/" keywords = ["json", "serde", "serialization"] categories = ["encoding"] readme = "README.md" -include = ["build.rs", "src/**/*.rs", "README.md", "LICENSE-APACHE", "LICENSE-MIT"] edition = "2018" rust-version = "1.36" @@ -21,6 +20,7 @@ ryu = "1.0" [dev-dependencies] automod = "1.0" +ref-cast = "1.0" rustversion = "1.0" serde_bytes = "0.11" serde_derive = "1.0" diff --git a/README.md b/README.md index 27a365372..6d8af6945 100644 --- a/README.md +++ b/README.md @@ -330,17 +330,20 @@ Benchmarks live in the [serde-rs/json-benchmark] repo. Serde is one of the most widely used Rust libraries so any place that Rustaceans congregate will be able to help you out. For chat, consider trying the -[#general] or [#beginners] channels of the unofficial community Discord, the -[#rust-usage] channel of the official Rust Project Discord, or the -[#general][zulip] stream in Zulip. For asynchronous, consider the [\[rust\] tag -on StackOverflow][stackoverflow], the [/r/rust] subreddit which has a pinned -weekly easy questions post, or the Rust [Discourse forum][discourse]. It's -acceptable to file a support issue in this repo but they tend not to get as many -eyes as any of the above and may get closed without a response after some time. - -[#general]: https://discord.com/channels/273534239310479360/274215136414400513 -[#beginners]: https://discord.com/channels/273534239310479360/273541522815713281 +[#rust-questions] or [#rust-beginners] channels of the unofficial community +Discord (invite: ), the [#rust-usage] or +[#beginners] channels of the official Rust Project Discord (invite: +), or the [#general][zulip] stream in Zulip. For +asynchronous, consider the [\[rust\] tag on StackOverflow][stackoverflow], the +[/r/rust] subreddit which has a pinned weekly easy questions post, or the Rust +[Discourse forum][discourse]. It's acceptable to file a support issue in this +repo but they tend not to get as many eyes as any of the above and may get +closed without a response after some time. + +[#rust-questions]: https://discord.com/channels/273534239310479360/274215136414400513 +[#rust-beginners]: https://discord.com/channels/273534239310479360/273541522815713281 [#rust-usage]: https://discord.com/channels/442252698964721669/443150878111694848 +[#beginners]: https://discord.com/channels/442252698964721669/448238009733742612 [zulip]: https://rust-lang.zulipchat.com/#narrow/stream/122651-general [stackoverflow]: https://stackoverflow.com/questions/tagged/rust [/r/rust]: https://www.reddit.com/r/rust diff --git a/src/de.rs b/src/de.rs index b4c5ef7dc..ffd0d48c2 100644 --- a/src/de.rs +++ b/src/de.rs @@ -864,6 +864,15 @@ impl<'de, R: Read<'de>> Deserializer { buf.push('-'); } self.scan_integer(&mut buf)?; + if positive { + if let Ok(unsigned) = buf.parse() { + return Ok(ParserNumber::U64(unsigned)); + } + } else { + if let Ok(signed) = buf.parse() { + return Ok(ParserNumber::I64(signed)); + } + } Ok(ParserNumber::String(buf)) } @@ -2175,10 +2184,18 @@ where } #[inline] - fn deserialize_newtype_struct(self, _name: &'static str, visitor: V) -> Result + fn deserialize_newtype_struct(self, name: &'static str, visitor: V) -> Result where V: de::Visitor<'de>, { + #[cfg(feature = "raw_value")] + { + if name == crate::raw::TOKEN { + return self.de.deserialize_raw_value(visitor); + } + } + + let _ = name; visitor.visit_newtype_struct(self) } diff --git a/src/lib.rs b/src/lib.rs index 47671b4bf..03fa1d891 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -300,9 +300,10 @@ //! [macro]: https://docs.serde.rs/serde_json/macro.json.html //! [`serde-json-core`]: https://github.com/rust-embedded-community/serde-json-core -#![doc(html_root_url = "https://docs.rs/serde_json/1.0.74")] +#![doc(html_root_url = "https://docs.rs/serde_json/1.0.78")] // Ignored clippy lints #![allow( + clippy::collapsible_else_if, clippy::comparison_chain, clippy::deprecated_cfg_attr, clippy::doc_markdown, @@ -321,6 +322,8 @@ )] // Ignored clippy_pedantic lints #![allow( + // buggy + clippy::iter_not_returning_iterator, // https://github.com/rust-lang/rust-clippy/issues/8285 // Deserializer::from_str, into_iter clippy::should_implement_trait, // integer and float ser/de requires these sorts of casts diff --git a/src/raw.rs b/src/raw.rs index 8f11a06d3..c8377ac82 100644 --- a/src/raw.rs +++ b/src/raw.rs @@ -1,4 +1,7 @@ use crate::error::Error; +use alloc::borrow::ToOwned; +use alloc::boxed::Box; +use alloc::string::String; use core::fmt::{self, Debug, Display}; use core::mem; use serde::de::value::BorrowedStrDeserializer; @@ -446,9 +449,10 @@ impl<'de> Visitor<'de> for BoxedFromString { where E: de::Error, { - self.visit_string(s.to_owned()) + Ok(RawValue::from_owned(s.to_owned().into_boxed_str())) } + #[cfg(any(feature = "std", feature = "alloc"))] fn visit_string(self, s: String) -> Result where E: de::Error, diff --git a/src/ser.rs b/src/ser.rs index b497c56c8..db77cd883 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -1548,6 +1548,13 @@ impl<'a, W: io::Write, F: Formatter> ser::Serializer for RawValueStrEmitter<'a, ) -> Result { Err(ser::Error::custom("expected RawValue")) } + + fn collect_str(self, value: &T) -> Result + where + T: ?Sized + Display, + { + self.serialize_str(&value.to_string()) + } } /// Represents a character escape code in a type-safe manner. diff --git a/src/value/de.rs b/src/value/de.rs index 347dbee96..75e49df55 100644 --- a/src/value/de.rs +++ b/src/value/de.rs @@ -4,6 +4,8 @@ use crate::number::Number; use crate::value::Value; use alloc::borrow::{Cow, ToOwned}; use alloc::string::String; +#[cfg(feature = "raw_value")] +use alloc::string::ToString; use alloc::vec::{self, Vec}; use core::fmt; use core::slice; diff --git a/src/value/ser.rs b/src/value/ser.rs index 5f59c6373..179380a51 100644 --- a/src/value/ser.rs +++ b/src/value/ser.rs @@ -1020,4 +1020,11 @@ impl serde::ser::Serializer for RawValueEmitter { ) -> Result { Err(invalid_raw_value()) } + + fn collect_str(self, value: &T) -> Result + where + T: ?Sized + Display, + { + self.serialize_str(&value.to_string()) + } } diff --git a/tests/regression/issue845.rs b/tests/regression/issue845.rs new file mode 100644 index 000000000..dcca55694 --- /dev/null +++ b/tests/regression/issue845.rs @@ -0,0 +1,72 @@ +use serde::{Deserialize, Deserializer}; +use std::convert::TryFrom; +use std::fmt::{self, Display}; +use std::marker::PhantomData; +use std::str::FromStr; + +pub struct NumberVisitor { + marker: PhantomData, +} + +impl<'de, T> serde::de::Visitor<'de> for NumberVisitor +where + T: TryFrom + TryFrom + FromStr, + >::Error: Display, + >::Error: Display, + ::Err: Display, +{ + type Value = T; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an integer or string") + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + T::try_from(v).map_err(serde::de::Error::custom) + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + T::try_from(v).map_err(serde::de::Error::custom) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + v.parse().map_err(serde::de::Error::custom) + } +} + +fn deserialize_integer_or_string<'de, D, T>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: TryFrom + TryFrom + FromStr, + >::Error: Display, + >::Error: Display, + ::Err: Display, +{ + deserializer.deserialize_any(NumberVisitor { + marker: PhantomData, + }) +} + +#[derive(Deserialize, Debug)] +pub struct Struct { + #[serde(deserialize_with = "deserialize_integer_or_string")] + pub i: i64, +} + +#[test] +fn test() { + let j = r#" {"i":100} "#; + println!("{:?}", serde_json::from_str::(j).unwrap()); + + let j = r#" {"i":"100"} "#; + println!("{:?}", serde_json::from_str::(j).unwrap()); +} diff --git a/tests/test.rs b/tests/test.rs index 054e96088..304007a37 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -19,6 +19,8 @@ trace_macros!(true); #[macro_use] mod macros; +#[cfg(feature = "raw_value")] +use ref_cast::RefCast; use serde::de::{self, IgnoredAny, IntoDeserializer}; use serde::ser::{self, SerializeMap, SerializeSeq, Serializer}; use serde::{Deserialize, Serialize}; @@ -31,6 +33,8 @@ use serde_json::{ }; use std::collections::hash_map::DefaultHasher; use std::collections::BTreeMap; +#[cfg(feature = "raw_value")] +use std::collections::HashMap; use std::fmt::{self, Debug}; use std::hash::{Hash, Hasher}; use std::io; @@ -715,11 +719,7 @@ fn test_parse_char() { ), ( "10", - if cfg!(feature = "arbitrary_precision") { - "invalid type: number, expected a character at line 1 column 2" - } else { - "invalid type: integer `10`, expected a character at line 1 column 2" - }, + "invalid type: integer `10`, expected a character at line 1 column 2", ), ]); @@ -1203,11 +1203,7 @@ fn test_parse_struct() { test_parse_err::(&[ ( "5", - if cfg!(feature = "arbitrary_precision") { - "invalid type: number, expected struct Outer at line 1 column 1" - } else { - "invalid type: integer `5`, expected struct Outer at line 1 column 1" - }, + "invalid type: integer `5`, expected struct Outer at line 1 column 1", ), ( "\"hello\"", @@ -2201,6 +2197,44 @@ fn test_borrowed_raw_value() { assert_eq!(r#"["a",42,{"foo": "bar"},null]"#, array_to_string); } +#[cfg(feature = "raw_value")] +#[test] +fn test_raw_value_in_map_key() { + #[derive(RefCast)] + #[repr(transparent)] + struct RawMapKey(RawValue); + + impl<'de> Deserialize<'de> for &'de RawMapKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let raw_value = <&RawValue>::deserialize(deserializer)?; + Ok(RawMapKey::ref_cast(raw_value)) + } + } + + impl PartialEq for RawMapKey { + fn eq(&self, other: &Self) -> bool { + self.0.get() == other.0.get() + } + } + + impl Eq for RawMapKey {} + + impl Hash for RawMapKey { + fn hash(&self, hasher: &mut H) { + self.0.get().hash(hasher); + } + } + + let map_from_str: HashMap<&RawMapKey, &RawValue> = + serde_json::from_str(r#" {"\\k":"\\v"} "#).unwrap(); + let (map_k, map_v) = map_from_str.into_iter().next().unwrap(); + assert_eq!("\"\\\\k\"", map_k.0.get()); + assert_eq!("\"\\\\v\"", map_v.get()); +} + #[cfg(feature = "raw_value")] #[test] fn test_boxed_raw_value() {