diff --git a/build.gradle b/build.gradle index 6a20ee7201..59a1f41042 100644 --- a/build.gradle +++ b/build.gradle @@ -654,6 +654,7 @@ tasks.register('generateRustTestCodecs', JavaExec) { 'sbe-tool/src/test/resources/issue984.xml', 'sbe-tool/src/test/resources/issue987.xml', 'sbe-tool/src/test/resources/issue1028.xml', + 'sbe-tool/src/test/resources/fixed-sized-primitive-array-types.xml', 'sbe-tool/src/test/resources/example-bigendian-test-schema.xml', 'sbe-tool/src/test/resources/nested-composite-name.xml', ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index f15d4d2f6d..2d4f3d4a49 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -19,6 +19,7 @@ issue_987 = { path = "../generated/rust/issue987" } issue_1028 = { path = "../generated/rust/issue1028" } baseline_bigendian = { path = "../generated/rust/baseline-bigendian" } nested_composite_name = { path = "../generated/rust/nested-composite-name" } +fixed_sized_primitive_array = { path = "../generated/rust/fixed_sized_primitive_array" } [dev-dependencies] criterion = "0.5" diff --git a/rust/tests/fixed_sized_primitive_array.rs b/rust/tests/fixed_sized_primitive_array.rs new file mode 100644 index 0000000000..43e82f347d --- /dev/null +++ b/rust/tests/fixed_sized_primitive_array.rs @@ -0,0 +1,1110 @@ +use fixed_sized_primitive_array::{ + demo_codec::{DemoDecoder, DemoEncoder}, + message_header_codec::{MessageHeaderDecoder, ENCODED_LENGTH}, + ReadBuf, WriteBuf, +}; + +fn create_encoder(buffer: &mut [u8]) -> DemoEncoder { + let encoder = DemoEncoder::default().wrap(WriteBuf::new(buffer), ENCODED_LENGTH); + let mut header = encoder.header(0); + header.parent().unwrap() +} + +#[test] +fn test_encode_then_decode_u8_slice() { + let uninit = 1u8; + + let test_data = [ + b"" as &[u8], + b"0" as &[u8], + b"01" as &[u8], + b"012" as &[u8], + b"0123" as &[u8], + b"01234" as &[u8], + b"012345" as &[u8], + b"0123456" as &[u8], + b"01234567" as &[u8], + b"012345678" as &[u8], + b"0123456789" as &[u8], + b"0123456789A" as &[u8], + b"0123456789AB" as &[u8], + b"0123456789ABC" as &[u8], + b"0123456789ABCD" as &[u8], + b"0123456789ABCDE" as &[u8], + b"0123456789ABCDEF" as &[u8], + b"0123456789abcdef" as &[u8], + b"0123456789abcdef0" as &[u8], + b"0123456789abcdef01" as &[u8], + b"0123456789abcdef012" as &[u8], + b"0123456789abcdef0123" as &[u8], + b"0123456789abcdef01234" as &[u8], + b"0123456789abcdef012345" as &[u8], + b"0123456789abcdef0123456" as &[u8], + b"0123456789abcdef01234567" as &[u8], + b"0123456789abcdef012345678" as &[u8], + b"0123456789abcdef0123456789" as &[u8], + b"0123456789abcdef0123456789A" as &[u8], + b"0123456789abcdef0123456789AB" as &[u8], + b"0123456789abcdef0123456789ABC" as &[u8], + b"0123456789abcdef0123456789ABCD" as &[u8], + b"0123456789abcdef0123456789ABCDE" as &[u8], + b"0123456789abcdef0123456789ABCDEF" as &[u8], + ]; + + // + // + // + // + // + // + // + // + macro_rules! run_encode_then_decode_for_array_of_u8_len_16 { + ($encode_func:expr, $decode_func:expr, $end_encode_func:expr, $end_decode_func:expr) => { + for each_slice in test_data { + let encode_func = $encode_func; + let decode_func = $decode_func; + let end_encode_func = $end_encode_func; + let end_decode_func = $end_decode_func; + + let cur_len = each_slice.len(); + let effective_len = cur_len.min(16); + + // encode... + let mut buffer = [uninit; 1024]; + let mut encoder = create_encoder(&mut buffer); + + encode_func(&mut encoder, each_slice); + + let end = 1i32; + end_encode_func(&mut encoder, end); + + // decode... + let buf = ReadBuf::new(buffer.as_slice()); + let header = MessageHeaderDecoder::default().wrap(buf, 0); + + let decoder = DemoDecoder::default().header(header, 0); + let decoded = decode_func(&decoder); + for each_idx in 0..effective_len { + assert_eq!( + each_slice[each_idx], decoded[each_idx], + "Item mismatched at {}/{}", + each_idx, cur_len + ); + } + for each_idx in effective_len..16 { + assert_eq!( + decoded[each_idx], uninit, + "Item should not be padded ZERO at {}/{}", + each_idx, cur_len + ); + } + let decoded_end = end_decode_func(&decoder); + assert_eq!(decoded_end, end, "End Item should equal",); + } + }; + } + + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_char_at_most_16_items_from_slice, + DemoDecoder::fixed_16_char, + DemoEncoder::fixed_16_char_end, + DemoDecoder::fixed_16_char_end + ); + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_ascii_char_at_most_16_items_from_slice, + DemoDecoder::fixed_16_ascii_char, + DemoEncoder::fixed_16_ascii_char_end, + DemoDecoder::fixed_16_ascii_char_end + ); + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_gb_18030_char_at_most_16_items_from_slice, + DemoDecoder::fixed_16_gb_18030_char, + DemoEncoder::fixed_16_gb_18030_char_end, + DemoDecoder::fixed_16_gb_18030_char_end + ); + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_utf_8_char_at_most_16_items_from_slice, + DemoDecoder::fixed_16_utf_8_char, + DemoEncoder::fixed_16_utf_8_char_end, + DemoDecoder::fixed_16_utf_8_char_end + ); + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_u8_at_most_16_items_from_slice, + DemoDecoder::fixed_16_u8, + DemoEncoder::fixed_16_u8_end, + DemoDecoder::fixed_16_u8_end + ); + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_ascii_u8_at_most_16_items_from_slice, + DemoDecoder::fixed_16_ascii_u8, + DemoEncoder::fixed_16_ascii_u8_end, + DemoDecoder::fixed_16_ascii_u8_end + ); + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_gb_18030_u8_at_most_16_items_from_slice, + DemoDecoder::fixed_16_gb_18030_u8, + DemoEncoder::fixed_16_gb_18030_u8_end, + DemoDecoder::fixed_16_gb_18030_u8_end + ); + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_utf_8_u8_at_most_16_items_from_slice, + DemoDecoder::fixed_16_utf_8_u8, + DemoEncoder::fixed_16_utf_8u_8_end, + DemoDecoder::fixed_16_utf_8u_8_end + ); +} + +#[test] +fn test_encode_then_decode_non_u8_signed_primitive_slice() { + let uninit = 1u8; + + // + // + // + // + macro_rules! run_encode_then_decode_for_array_of_signed_len_16 { + ($encode_func:expr, $decode_func:expr, $end_encode_func:expr, $end_decode_func:expr, $i_type:ty, $existed:expr) => { + let test_data = [ + &[] as &[$i_type], + &[1 as $i_type] as &[$i_type], + &[0 as $i_type] as &[$i_type], + &[-1 as $i_type] as &[$i_type], + &[-1, 1 as $i_type] as &[$i_type], + &[-1, 0, 1 as $i_type] as &[$i_type], + &[-2, -1, 1, 2 as $i_type] as &[$i_type], + &[-2, -1, 0, 1, 2 as $i_type] as &[$i_type], + &[-3, -2, -1, 1, 2, 3 as $i_type] as &[$i_type], + &[-3, -2, -1, 0, 1, 2, 3 as $i_type] as &[$i_type], + &[-4, -3, -2, -1, 1, 2, 3, 4 as $i_type] as &[$i_type], + &[-4, -3, -2, -1, 0, 1, 2, 3, 4 as $i_type] as &[$i_type], + &[-5, -4, -3, -2, -1, 1, 2, 3, 4, 5 as $i_type] as &[$i_type], + &[-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5 as $i_type] as &[$i_type], + &[-6, -5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 6 as $i_type] as &[$i_type], + &[-6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6 as $i_type] as &[$i_type], + &[-7, -6, -5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 6, 7 as $i_type] as &[$i_type], + &[ + -7, + -6, + -5, + -4, + -3, + -2, + -1, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 as $i_type, + ] as &[$i_type], + &[ + -8, + -7, + -6, + -5, + -4, + -3, + -2, + -1, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 as $i_type, + ] as &[$i_type], + &[ + -8, + -7, + -6, + -5, + -4, + -3, + -2, + -1, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 as $i_type, + ] as &[$i_type], + &[ + -9, + -8, + -7, + -6, + -5, + -4, + -3, + -2, + -1, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 as $i_type, + ] as &[$i_type], + &[ + -9, + -8, + -7, + -6, + -5, + -4, + -3, + -2, + -1, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 as $i_type, + ] as &[$i_type], + ]; + + let existed = $existed; + + for each_slice in test_data { + let encode_func = $encode_func; + let decode_func = $decode_func; + let end_encode_func = $end_encode_func; + let end_decode_func = $end_decode_func; + + let cur_len = each_slice.len(); + let effective_len = cur_len.min(16); + + // encode... + let mut buffer = [uninit; 1024]; + let mut encoder = create_encoder(&mut buffer); + + encode_func(&mut encoder, &each_slice); + + let end = 1i32; + end_encode_func(&mut encoder, end); + + // decode... + let buf = ReadBuf::new(buffer.as_slice()); + let header = MessageHeaderDecoder::default().wrap(buf, 0); + + let decoder = DemoDecoder::default().header(header, 0); + let decoded = decode_func(&decoder); + for each_idx in 0..effective_len { + assert_eq!( + each_slice[each_idx], + decoded[each_idx], + "Item mismatched at {}/{} for {}", + each_idx, + cur_len, + stringify!($i_type) + ); + } + for each_idx in effective_len..16 { + assert_eq!( + decoded[each_idx], + existed, + "Item should not be padded ZERO at {}/{} for {}", + each_idx, + cur_len, + stringify!($i_type) + ); + } + let decoded_end = end_decode_func(&decoder); + assert_eq!(decoded_end, end, "End Item should equal",); + } + }; + } + + run_encode_then_decode_for_array_of_signed_len_16!( + DemoEncoder::fixed_16_i8_at_most_16_items_from_slice, + DemoDecoder::fixed_16_i8, + DemoEncoder::fixed_16_i8_end, + DemoDecoder::fixed_16_i8_end, + i8, i8::from_le_bytes([uninit]) + ); + run_encode_then_decode_for_array_of_signed_len_16!( + DemoEncoder::fixed_16_i16_at_most_16_items_from_slice, + DemoDecoder::fixed_16_i16, + DemoEncoder::fixed_16_i16_end, + DemoDecoder::fixed_16_i16_end, + i16, i16::from_le_bytes([uninit, uninit]) + ); + run_encode_then_decode_for_array_of_signed_len_16!( + DemoEncoder::fixed_16_i32_at_most_16_items_from_slice, + DemoDecoder::fixed_16_i32, + DemoEncoder::fixed_16_i32_end, + DemoDecoder::fixed_16_i32_end, + i32, i32::from_le_bytes([uninit, uninit, uninit, uninit]) + ); + run_encode_then_decode_for_array_of_signed_len_16!( + DemoEncoder::fixed_16_i64_at_most_16_items_from_slice, + DemoDecoder::fixed_16_i64, + DemoEncoder::fixed_16_i64_end, + DemoDecoder::fixed_16_i64_end, + i64, i64::from_le_bytes([uninit, uninit, uninit, uninit, uninit, uninit, uninit, uninit]) + ); +} + +#[test] +fn test_encode_then_decode_non_u8_unsigned_primitive_slice() { + let uninit = 1u8; + + // + // + macro_rules! run_encode_then_decode_for_array_of_unsigned_len_16 { + ($encode_func:expr, $decode_func:expr, $end_encode_func:expr, $end_decode_func:expr, $i_type:ty, $existed:expr) => { + let test_data = [ + &[] as &[$i_type], + &[1 as $i_type] as &[$i_type], + &[0 as $i_type] as &[$i_type], + &[11 as $i_type] as &[$i_type], + &[11, 1 as $i_type] as &[$i_type], + &[11, 0, 1 as $i_type] as &[$i_type], + &[12, 11, 1, 2 as $i_type] as &[$i_type], + &[12, 11, 0, 1, 2 as $i_type] as &[$i_type], + &[13, 12, 11, 1, 2, 3 as $i_type] as &[$i_type], + &[13, 12, 11, 0, 1, 2, 3 as $i_type] as &[$i_type], + &[14, 13, 12, 11, 1, 2, 3, 4 as $i_type] as &[$i_type], + &[14, 13, 12, 11, 0, 1, 2, 3, 4 as $i_type] as &[$i_type], + &[15, 14, 13, 12, 11, 1, 2, 3, 4, 5 as $i_type] as &[$i_type], + &[15, 14, 13, 12, 11, 0, 1, 2, 3, 4, 5 as $i_type] as &[$i_type], + &[16, 15, 14, 13, 12, 11, 1, 2, 3, 4, 5, 6 as $i_type] as &[$i_type], + &[16, 15, 14, 13, 12, 11, 0, 1, 2, 3, 4, 5, 6 as $i_type] as &[$i_type], + &[17, 16, 15, 14, 13, 12, 11, 1, 2, 3, 4, 5, 6, 7 as $i_type] as &[$i_type], + &[ + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 as $i_type, + ] as &[$i_type], + &[ + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 as $i_type, + ] as &[$i_type], + &[ + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 as $i_type, + ] as &[$i_type], + &[ + 19, + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 as $i_type, + ] as &[$i_type], + &[ + 19, + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 as $i_type, + ] as &[$i_type], + ]; + + let existed = $existed; + + for each_slice in test_data { + let encode_func = $encode_func; + let decode_func = $decode_func; + let end_encode_func = $end_encode_func; + let end_decode_func = $end_decode_func; + + let cur_len = each_slice.len(); + let effective_len = cur_len.min(16); + + // encode... + let mut buffer = [uninit; 1024]; + let mut encoder = create_encoder(&mut buffer); + + encode_func(&mut encoder, each_slice); + + let end = 1i32; + end_encode_func(&mut encoder, end); + + // decode... + let buf = ReadBuf::new(buffer.as_slice()); + let header = MessageHeaderDecoder::default().wrap(buf, 0); + + let decoder = DemoDecoder::default().header(header, 0); + let decoded = decode_func(&decoder); + for each_idx in 0..effective_len { + assert_eq!( + each_slice[each_idx], + decoded[each_idx], + "Item mismatched at {}/{} for {}", + each_idx, + cur_len, + stringify!($i_type) + ); + } + + for each_idx in effective_len..16 { + assert_eq!( + decoded[each_idx], + existed, + "Item should not be padded ZERO at {}/{} for {}", + each_idx, + cur_len, + stringify!($i_type) + ); + } + let decoded_end = end_decode_func(&decoder); + assert_eq!(decoded_end, end, "End Item should equal",); + } + }; + } + + run_encode_then_decode_for_array_of_unsigned_len_16!( + DemoEncoder::fixed_16_u16_at_most_16_items_from_slice, + DemoDecoder::fixed_16_u16, + DemoEncoder::fixed_16_u16_end, + DemoDecoder::fixed_16_u16_end, + u16, + u16::from_le_bytes([uninit, uninit]) + ); + run_encode_then_decode_for_array_of_unsigned_len_16!( + DemoEncoder::fixed_16_u32_at_most_16_items_from_slice, + DemoDecoder::fixed_16_u32, + DemoEncoder::fixed_16_u32_end, + DemoDecoder::fixed_16_u32_end, + u32, + u32::from_le_bytes([uninit, uninit, uninit, uninit]) + ); + run_encode_then_decode_for_array_of_unsigned_len_16!( + DemoEncoder::fixed_16_u64_at_most_16_items_from_slice, + DemoDecoder::fixed_16_u64, + DemoEncoder::fixed_16_u64_end, + DemoDecoder::fixed_16_u64_end, + u64, + u64::from_le_bytes([uninit, uninit, uninit, uninit, uninit, uninit, uninit, uninit]) + ); +} + +#[test] +fn test_encode_then_decode_u8_slice_padded() { + let uninit = 1u8; + + let test_data = [ + b"" as &[u8], + b"0" as &[u8], + b"01" as &[u8], + b"012" as &[u8], + b"0123" as &[u8], + b"01234" as &[u8], + b"012345" as &[u8], + b"0123456" as &[u8], + b"01234567" as &[u8], + b"012345678" as &[u8], + b"0123456789" as &[u8], + b"0123456789A" as &[u8], + b"0123456789AB" as &[u8], + b"0123456789ABC" as &[u8], + b"0123456789ABCD" as &[u8], + b"0123456789ABCDE" as &[u8], + b"0123456789ABCDEF" as &[u8], + b"0123456789abcdef" as &[u8], + b"0123456789abcdef0" as &[u8], + b"0123456789abcdef01" as &[u8], + b"0123456789abcdef012" as &[u8], + b"0123456789abcdef0123" as &[u8], + b"0123456789abcdef01234" as &[u8], + b"0123456789abcdef012345" as &[u8], + b"0123456789abcdef0123456" as &[u8], + b"0123456789abcdef01234567" as &[u8], + b"0123456789abcdef012345678" as &[u8], + b"0123456789abcdef0123456789" as &[u8], + b"0123456789abcdef0123456789A" as &[u8], + b"0123456789abcdef0123456789AB" as &[u8], + b"0123456789abcdef0123456789ABC" as &[u8], + b"0123456789abcdef0123456789ABCD" as &[u8], + b"0123456789abcdef0123456789ABCDE" as &[u8], + b"0123456789abcdef0123456789ABCDEF" as &[u8], + ]; + + // + // + // + // + // + // + // + // + macro_rules! run_encode_then_decode_for_array_of_u8_len_16 { + ($encode_func:expr, $decode_func:expr, $end_encode_func:expr, $end_decode_func:expr,) => { + for each_slice in test_data { + let encode_func = $encode_func; + let decode_func = $decode_func; + let end_encode_func = $end_encode_func; + let end_decode_func = $end_decode_func; + + let cur_len = each_slice.len(); + let effective_len = cur_len.min(16); + + // encode... + let mut buffer = [uninit; 1024]; + let mut encoder = create_encoder(&mut buffer); + + encode_func(&mut encoder, each_slice); + + let end = 1i32; + end_encode_func(&mut encoder, end); + + // decode... + let buf = ReadBuf::new(buffer.as_slice()); + let header = MessageHeaderDecoder::default().wrap(buf, 0); + + let decoder = DemoDecoder::default().header(header, 0); + let decoded = decode_func(&decoder); + for each_idx in 0..effective_len { + assert_eq!( + each_slice[each_idx], decoded[each_idx], + "Item mismatched at {}/{}", + each_idx, cur_len + ); + } + for each_idx in effective_len..16 { + assert_eq!( + decoded[each_idx], 0, + "Item should be padded ZERO at {}/{}", + each_idx, cur_len + ); + } + let decoded_end = end_decode_func(&decoder); + assert_eq!(decoded_end, end, "End Item should equal",); + } + }; + } + + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_char_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_char, + DemoEncoder::fixed_16_char_end, + DemoDecoder::fixed_16_char_end, + ); + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_ascii_char_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_ascii_char, + DemoEncoder::fixed_16_ascii_char_end, + DemoDecoder::fixed_16_ascii_char_end, + ); + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_gb_18030_char_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_gb_18030_char, + DemoEncoder::fixed_16_gb_18030_char_end, + DemoDecoder::fixed_16_gb_18030_char_end, + ); + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_utf_8_char_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_utf_8_char, + DemoEncoder::fixed_16_utf_8_char_end, + DemoDecoder::fixed_16_utf_8_char_end, + ); + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_u8_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_u8, + DemoEncoder::fixed_16_u8_end, + DemoDecoder::fixed_16_u8_end, + ); + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_ascii_u8_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_ascii_u8, + DemoEncoder::fixed_16_ascii_u8_end, + DemoDecoder::fixed_16_ascii_u8_end, + ); + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_gb_18030_u8_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_gb_18030_u8, + DemoEncoder::fixed_16_gb_18030_u8_end, + DemoDecoder::fixed_16_gb_18030_u8_end, + ); + run_encode_then_decode_for_array_of_u8_len_16!( + DemoEncoder::fixed_16_utf_8_u8_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_utf_8_u8, + DemoEncoder::fixed_16_utf_8u_8_end, + DemoDecoder::fixed_16_utf_8u_8_end, + ); +} + +#[test] +fn test_encode_then_decode_non_u8_signed_primitive_slice_padded() { + let uninit = 1u8; + + // + // + // + // + macro_rules! run_encode_then_decode_for_array_of_signed_len_16 { + ($encode_func:expr, $decode_func:expr, $end_encode_func:expr, $end_decode_func:expr, $i_type:ty,) => { + let test_data = [ + &[] as &[$i_type], + &[1 as $i_type] as &[$i_type], + &[0 as $i_type] as &[$i_type], + &[-1 as $i_type] as &[$i_type], + &[-1, 1 as $i_type] as &[$i_type], + &[-1, 0, 1 as $i_type] as &[$i_type], + &[-2, -1, 1, 2 as $i_type] as &[$i_type], + &[-2, -1, 0, 1, 2 as $i_type] as &[$i_type], + &[-3, -2, -1, 1, 2, 3 as $i_type] as &[$i_type], + &[-3, -2, -1, 0, 1, 2, 3 as $i_type] as &[$i_type], + &[-4, -3, -2, -1, 1, 2, 3, 4 as $i_type] as &[$i_type], + &[-4, -3, -2, -1, 0, 1, 2, 3, 4 as $i_type] as &[$i_type], + &[-5, -4, -3, -2, -1, 1, 2, 3, 4, 5 as $i_type] as &[$i_type], + &[-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5 as $i_type] as &[$i_type], + &[-6, -5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 6 as $i_type] as &[$i_type], + &[-6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6 as $i_type] as &[$i_type], + &[-7, -6, -5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 6, 7 as $i_type] as &[$i_type], + &[ + -7, + -6, + -5, + -4, + -3, + -2, + -1, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 as $i_type, + ] as &[$i_type], + &[ + -8, + -7, + -6, + -5, + -4, + -3, + -2, + -1, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 as $i_type, + ] as &[$i_type], + &[ + -8, + -7, + -6, + -5, + -4, + -3, + -2, + -1, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 as $i_type, + ] as &[$i_type], + &[ + -9, + -8, + -7, + -6, + -5, + -4, + -3, + -2, + -1, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 as $i_type, + ] as &[$i_type], + &[ + -9, + -8, + -7, + -6, + -5, + -4, + -3, + -2, + -1, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 as $i_type, + ] as &[$i_type], + ]; + for each_slice in test_data { + let encode_func = $encode_func; + let decode_func = $decode_func; + let end_encode_func = $end_encode_func; + let end_decode_func = $end_decode_func; + + let cur_len = each_slice.len(); + let effective_len = cur_len.min(16); + + // encode... + let mut buffer = [uninit; 1024]; + let mut encoder = create_encoder(&mut buffer); + + encode_func(&mut encoder, each_slice); + + let end = 1i32; + end_encode_func(&mut encoder, end); + + // decode... + let buf = ReadBuf::new(buffer.as_slice()); + let header = MessageHeaderDecoder::default().wrap(buf, 0); + + let decoder = DemoDecoder::default().header(header, 0); + let decoded = decode_func(&decoder); + for each_idx in 0..effective_len { + assert_eq!( + each_slice[each_idx], + decoded[each_idx], + "Item mismatched at {}/{} for {}", + each_idx, + cur_len, + stringify!($i_type) + ); + } + for each_idx in effective_len..16 { + assert_eq!( + decoded[each_idx], + 0, + "Item should be padded ZERO at {}/{} for {}", + each_idx, + cur_len, + stringify!($i_type) + ); + } + let decoded_end = end_decode_func(&decoder); + assert_eq!(decoded_end, end, "End Item should equal",); + } + }; + } + + run_encode_then_decode_for_array_of_signed_len_16!( + DemoEncoder::fixed_16_i8_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_i8, + DemoEncoder::fixed_16_i8_end, + DemoDecoder::fixed_16_i8_end, + i8, + ); + run_encode_then_decode_for_array_of_signed_len_16!( + DemoEncoder::fixed_16_i16_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_i16, + DemoEncoder::fixed_16_i16_end, + DemoDecoder::fixed_16_i16_end, + i16, + ); + run_encode_then_decode_for_array_of_signed_len_16!( + DemoEncoder::fixed_16_i32_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_i32, + DemoEncoder::fixed_16_i32_end, + DemoDecoder::fixed_16_i32_end, + i32, + ); + run_encode_then_decode_for_array_of_signed_len_16!( + DemoEncoder::fixed_16_i64_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_i64, + DemoEncoder::fixed_16_i64_end, + DemoDecoder::fixed_16_i64_end, + i64, + ); +} + +#[test] +fn test_encode_then_decode_non_u8_unsigned_primitive_slice_padded() { + let uninit = 1u8; + + // + // + // + macro_rules! run_encode_then_decode_for_array_of_unsigned_len_16 { + ($encode_func:expr, $decode_func:expr, $end_encode_func:expr, $end_decode_func:expr, $i_type:ty) => { + let test_data = [ + &[] as &[$i_type], + &[1 as $i_type] as &[$i_type], + &[0 as $i_type] as &[$i_type], + &[11 as $i_type] as &[$i_type], + &[11, 1 as $i_type] as &[$i_type], + &[11, 0, 1 as $i_type] as &[$i_type], + &[12, 11, 1, 2 as $i_type] as &[$i_type], + &[12, 11, 0, 1, 2 as $i_type] as &[$i_type], + &[13, 12, 11, 1, 2, 3 as $i_type] as &[$i_type], + &[13, 12, 11, 0, 1, 2, 3 as $i_type] as &[$i_type], + &[14, 13, 12, 11, 1, 2, 3, 4 as $i_type] as &[$i_type], + &[14, 13, 12, 11, 0, 1, 2, 3, 4 as $i_type] as &[$i_type], + &[15, 14, 13, 12, 11, 1, 2, 3, 4, 5 as $i_type] as &[$i_type], + &[15, 14, 13, 12, 11, 0, 1, 2, 3, 4, 5 as $i_type] as &[$i_type], + &[16, 15, 14, 13, 12, 11, 1, 2, 3, 4, 5, 6 as $i_type] as &[$i_type], + &[16, 15, 14, 13, 12, 11, 0, 1, 2, 3, 4, 5, 6 as $i_type] as &[$i_type], + &[17, 16, 15, 14, 13, 12, 11, 1, 2, 3, 4, 5, 6, 7 as $i_type] as &[$i_type], + &[ + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 as $i_type, + ] as &[$i_type], + &[ + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 as $i_type, + ] as &[$i_type], + &[ + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 as $i_type, + ] as &[$i_type], + &[ + 19, + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 as $i_type, + ] as &[$i_type], + &[ + 19, + 18, + 17, + 16, + 15, + 14, + 13, + 12, + 11, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 as $i_type, + ] as &[$i_type], + ]; + for each_slice in test_data { + let encode_func = $encode_func; + let decode_func = $decode_func; + let end_encode_func = $end_encode_func; + let end_decode_func = $end_decode_func; + + let cur_len = each_slice.len(); + let effective_len = cur_len.min(16); + + // encode... + let mut buffer = [uninit; 1024]; + let mut encoder = create_encoder(&mut buffer); + + encode_func(&mut encoder, each_slice); + + let end = 1i32; + end_encode_func(&mut encoder, end); + + // decode... + let buf = ReadBuf::new(buffer.as_slice()); + let header = MessageHeaderDecoder::default().wrap(buf, 0); + + let decoder = DemoDecoder::default().header(header, 0); + let decoded = decode_func(&decoder); + for each_idx in 0..effective_len { + assert_eq!( + each_slice[each_idx], + decoded[each_idx], + "Item mismatched at {}/{} for {}", + each_idx, + cur_len, + stringify!($i_type) + ); + } + for each_idx in effective_len..16 { + assert_eq!( + decoded[each_idx], + 0, + "Item should be padded ZERO at {}/{} for {}", + each_idx, + cur_len, + stringify!($i_type) + ); + } + let decoded_end = end_decode_func(&decoder); + assert_eq!(decoded_end, end, "End Item should equal",); + } + }; + } + + run_encode_then_decode_for_array_of_unsigned_len_16!( + DemoEncoder::fixed_16_u16_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_u16, + DemoEncoder::fixed_16_u16_end, + DemoDecoder::fixed_16_u16_end, + u16 + ); + run_encode_then_decode_for_array_of_unsigned_len_16!( + DemoEncoder::fixed_16_u32_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_u32, + DemoEncoder::fixed_16_u32_end, + DemoDecoder::fixed_16_u32_end, + u32 + ); + run_encode_then_decode_for_array_of_unsigned_len_16!( + DemoEncoder::fixed_16_u64_at_most_16_items_from_slice_padded_with_zero, + DemoDecoder::fixed_16_u64, + DemoEncoder::fixed_16_u64_end, + DemoDecoder::fixed_16_u64_end, + u64 + ); +} diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustGenerator.java index f78899cb4b..2e4a4d5499 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustGenerator.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/rust/RustGenerator.java @@ -373,48 +373,12 @@ private static void generatePrimitiveEncoder( final int arrayLength = typeToken.arrayLength(); if (arrayLength > 1) { - indent(sb, level, "/// primitive array field '%s'\n", name); - indent(sb, level, "/// - min value: %s\n", encoding.applicableMinValue()); - indent(sb, level, "/// - max value: %s\n", encoding.applicableMaxValue()); - indent(sb, level, "/// - null value: %s\n", encoding.applicableNullValue()); - indent(sb, level, "/// - characterEncoding: %s\n", encoding.characterEncoding()); - indent(sb, level, "/// - semanticType: %s\n", encoding.semanticType()); - indent(sb, level, "/// - encodedOffset: %d\n", typeToken.offset()); - indent(sb, level, "/// - encodedLength: %d\n", typeToken.encodedLength()); - indent(sb, level, "/// - version: %d\n", typeToken.version()); - indent(sb, level, "#[inline]\n"); - indent(sb, level, "pub fn %s(&mut self, value: &[%s; %d]) {\n", - formatFunctionName(name), - rustPrimitiveType, - arrayLength); - - // NB: must create variable 'offset' before calling mutable self.get_buf_mut() - indent(sb, level + 1, "let offset = self.%s;\n", getBufOffset(typeToken)); - indent(sb, level + 1, "let buf = self.get_buf_mut();\n"); - - if (rustPrimitiveType.equals("u8")) - { - indent(sb, level + 1, "buf.put_bytes_at(offset, value);\n"); - indent(sb, level, "}\n\n"); - return; - } - - for (int i = 0; i < arrayLength; i++) - { - if (i == 0) - { - indent(sb, level + 1, "buf.put_%s_at(offset, value[%d]);\n", rustPrimitiveType, i); - } - else - { - indent(sb, level + 1, "buf.put_%s_at(offset + %d, value[%d]);\n", - rustPrimitiveType, - i * primitiveType.size(), - i); - } - } - - indent(sb, level, "}\n\n"); + generatePrimitiveArrayEncoderWithRef( + sb, level, typeToken, name, encoding, primitiveType, rustPrimitiveType); + generatePrimitiveArrayEncoderWithSliceRef( + sb, level, typeToken, name, encoding, primitiveType, rustPrimitiveType, false); + generatePrimitiveArrayEncoderWithSliceRef( + sb, level, typeToken, name, encoding, primitiveType, rustPrimitiveType, true); } else { @@ -446,6 +410,275 @@ private static void generatePrimitiveEncoder( } } + private static void generatePrimitiveArrayEncoderWithRef( + final StringBuilder sb, + final int level, + final Token typeToken, + final String name, + final Encoding encoding, + final PrimitiveType primitiveType, + final String rustPrimitiveType) throws IOException + { + final int arrayLength = typeToken.arrayLength(); + indent(sb, level, "/// primitive array field '%s'\n", name); + indent(sb, level, "/// - min value: %s\n", encoding.applicableMinValue()); + indent(sb, level, "/// - max value: %s\n", encoding.applicableMaxValue()); + indent(sb, level, "/// - null value: %s\n", encoding.applicableNullValue()); + indent(sb, level, "/// - characterEncoding: %s\n", encoding.characterEncoding()); + indent(sb, level, "/// - semanticType: %s\n", encoding.semanticType()); + indent(sb, level, "/// - encodedOffset: %d\n", typeToken.offset()); + indent(sb, level, "/// - encodedLength: %d\n", typeToken.encodedLength()); + indent(sb, level, "/// - version: %d\n", typeToken.version()); + indent(sb, level, "#[inline]\n"); + indent(sb, level, "pub fn %s(&mut self, value: &[%s; %d]) {\n", + formatFunctionName(name), rustPrimitiveType, arrayLength); + + // NB: must create variable 'offset' before calling mutable self.get_buf_mut() + indent(sb, level + 1, "let offset = self.%s;\n", getBufOffset(typeToken)); + indent(sb, level + 1, "let buf = self.get_buf_mut();\n"); + + if (rustPrimitiveType.equals("u8")) + { + indent(sb, level + 1, "buf.put_bytes_at(offset, value);\n"); + indent(sb, level, "}\n\n"); + return; + } + + for (int i = 0; i < arrayLength; i++) + { + if (i == 0) + { + indent(sb, level + 1, "buf.put_%s_at(offset, value[%d]);\n", rustPrimitiveType, i); + } + else + { + indent(sb, level + 1, "buf.put_%s_at(offset + %d, value[%d]);\n", + rustPrimitiveType, i * primitiveType.size(), i); + } + } + + indent(sb, level, "}\n\n"); + } + + private static void generatePrimitiveArrayEncoderWithSliceRefMethodBodyForU8( + final StringBuilder sb, + final int level, + final Token typeToken, + final String name, + final String rustPrimitiveType, + final boolean withPadding) throws IOException + { + final int arrayLength = typeToken.arrayLength(); + + indent(sb, level + 1, "match len {\n"); + + // Handle case when slice len is greater than 0 and less than {@code arrayLength} + indent(sb, level + 2, "(1..%d) => {\n", arrayLength); + indent(sb, level + 3, "// Copy all items from 'value' into '%s' of [%s; %d]\n", + name, rustPrimitiveType, arrayLength); + indent(sb, level + 3, "buf.put_slice_at(offset, value);\n"); + if (!withPadding) + { + indent(sb, level + 3, "// Leave all left items as is (no padding) in '%1$s' of [%2$s; %3$d]\n", + name, rustPrimitiveType, arrayLength); + } + else + { + indent(sb, level + 3, "// Pad all left items with ZERO(0%2$s) in '%1$s' of [%2$s; %3$d]\n", + name, rustPrimitiveType, arrayLength); + indent(sb, level + 3, "const ZEROS: [%1$s; %2$d] = [0%1$s; %2$d];\n", rustPrimitiveType, arrayLength); + indent(sb, level + 3, "buf.put_slice_at(offset + len, &ZEROS[len..]);\n"); + } + indent(sb, level + 2, "}\n"); + + // Handle case when slice is empty + indent(sb, level + 2, "0 => {\n"); + if (!withPadding) + { + indent(sb, level + 3, "// Leave all %3$d items as is (no padding) in '%1$s' of [%2$s; %3$d]\n", + name, rustPrimitiveType, arrayLength); + } + else + { + indent(sb, level + 3, "// Pad all %3$d items with ZERO(0%2$s) in '%s' of [%2$s; %3$d]\n", + name, rustPrimitiveType, arrayLength); + indent(sb, level + 3, "const ZEROS: [%1$s; %2$d] = [0%1$s; %2$d];\n", rustPrimitiveType, arrayLength); + indent(sb, level + 3, "buf.put_bytes_at(offset, &ZEROS);\n"); + } + indent(sb, level + 2, "}\n"); + + // Handle case when slice len is equal to or greater than {@code arrayLength} + indent(sb, level + 2, "_ /* >= %d */ => {\n", arrayLength); + indent(sb, level + 3, "// Copy at most %3$d items from 'value' into '%s' of [%s; %d]\n", + name, rustPrimitiveType, arrayLength); + indent(sb, level + 3, "buf.put_slice_at(offset, &value[..%d]);\n", arrayLength); + indent(sb, level + 2, "}\n"); + + indent(sb, level + 1, "}\n"); + } + + private static void generatePrimitiveArrayEncoderWithSliceRefMethodBodyForNonU8( + final StringBuilder sb, + final int level, + final Token typeToken, + final String name, + final PrimitiveType primitiveType, + final String rustPrimitiveType, + final boolean withPadding) throws IOException + { + final int arrayLength = typeToken.arrayLength(); + + indent(sb, level + 1, "match len {\n"); + + // Handle case when slice len is greater than 1 and less than {@code arrayLength} + if (arrayLength > 2) + { + indent(sb, level + 2, "(2..%d) => {\n", arrayLength); + indent(sb, level + 3, "// Copy all items from 'value' into '%s' of [%s; %d]\n", + name, rustPrimitiveType, arrayLength); + indent(sb, level + 3, "for each_i in 0..len {\n"); + indent(sb, level + 4, "buf.put_%s_at(offset + %d * each_i, value[each_i]);\n", + rustPrimitiveType, primitiveType.size()); + indent(sb, level + 3, "}\n"); + if (!withPadding) + { + indent(sb, level + 3, "// Leave all left items as is (no padding) in '%1$s' of [%2$s; %3$d]\n", + name, rustPrimitiveType, arrayLength); + } + else + { + indent(sb, level + 3, "// Pad all left items with ZERO(0%2$s) in '%1$s' of [%2$s; %3$d]\n", + name, rustPrimitiveType, arrayLength); + indent(sb, level + 3, "for each_i in len..%d {\n", arrayLength); + indent(sb, level + 4, "buf.put_%s_at(offset + %d * each_i, 0%1$s);\n", + rustPrimitiveType, primitiveType.size()); + indent(sb, level + 3, "}\n"); + } + indent(sb, level + 2, "}\n"); + } + + // Handle case when slice len is 1 (eliminate the need of loop) + indent(sb, level + 2, "1 => {\n"); + indent(sb, level + 3, "// Copy first 1 items from 'value' into '%s' of [%s; %d]\n", + name, rustPrimitiveType, arrayLength); + indent(sb, level + 3, "buf.put_%s_at(offset, value[0]);\n", rustPrimitiveType); + if (!withPadding) + { + indent(sb, level + 3, "// Leave all left %4$d items as is (no padding) in '%1$s' of [%2$s; %3$d]\n", + name, rustPrimitiveType, arrayLength, arrayLength - 1); + } + else + { + indent(sb, level + 3, "// Pad all left %4$d items with ZERO(0%2$s) in '%s' of [%2$s; %3$d]\n", + name, rustPrimitiveType, arrayLength, arrayLength - 1); + for (int i = 1; i < arrayLength; i++) + { + indent(sb, level + 3, "buf.put_%s_at(offset + %d, 0%1$s);\n", + rustPrimitiveType, i * primitiveType.size()); + } + } + indent(sb, level + 2, "}\n"); + + // Handle case when slice is empty + indent(sb, level + 2, "0 => {\n"); + if (!withPadding) + { + indent(sb, level + 3, "// Leave all %3$d items as is (no padding) in '%1$s' of [%2$s; %3$d]\n", + name, rustPrimitiveType, arrayLength); + } + else + { + indent(sb, level + 3, "// Pad all %3$d items with ZERO(0%2$s) in '%s' of [%2$s; %3$d]\n", + name, rustPrimitiveType, arrayLength); + for (int i = 0; i < arrayLength; i++) + { + if (i == 0) + { + indent(sb, level + 3, "buf.put_%s_at(offset, 0%1$s);\n", rustPrimitiveType); + } + else + { + indent(sb, level + 3, "buf.put_%s_at(offset + %d, 0%1$s);\n", + rustPrimitiveType, i * primitiveType.size()); + } + } + } + indent(sb, level + 2, "}\n"); + + // Handle case when slice len is equal to or greater than {@code arrayLength} + indent(sb, level + 2, "_ /* >= %d */ => {\n", arrayLength); + indent(sb, level + 3, "// Copy at most %3$d items from 'value' into '%s' of [%s; %d]\n", + name, rustPrimitiveType, arrayLength); + for (int i = 0; i < arrayLength; i++) + { + if (i == 0) + { + indent(sb, level + 3, "buf.put_%s_at(offset, value[%d]);\n", rustPrimitiveType, i); + } + else + { + indent(sb, level + 3, "buf.put_%s_at(offset + %d, value[%d]);\n", + rustPrimitiveType, i * primitiveType.size(), i); + } + } + indent(sb, level + 2, "}\n"); + + indent(sb, level + 1, "}\n"); + } + + private static void generatePrimitiveArrayEncoderWithSliceRef( + final StringBuilder sb, + final int level, + final Token typeToken, + final String name, + final Encoding encoding, + final PrimitiveType primitiveType, + final String rustPrimitiveType, + final boolean withPadding) throws IOException + { + final int arrayLength = typeToken.arrayLength(); + indent(sb, level, "/// primitive array field '%s': copy at most %d items from slice %s padding ZEROs\n", + name, arrayLength, withPadding ? "with" : "without"); + indent(sb, level, "/// - min value: %s\n", encoding.applicableMinValue()); + indent(sb, level, "/// - max value: %s\n", encoding.applicableMaxValue()); + indent(sb, level, "/// - null value: %s\n", encoding.applicableNullValue()); + indent(sb, level, "/// - characterEncoding: %s\n", encoding.characterEncoding()); + indent(sb, level, "/// - semanticType: %s\n", encoding.semanticType()); + indent(sb, level, "/// - encodedOffset: %d\n", typeToken.offset()); + indent(sb, level, "/// - encodedLength: %d\n", typeToken.encodedLength()); + indent(sb, level, "/// - version: %d\n", typeToken.version()); + indent(sb, level, "#[inline]\n"); + + if (!withPadding) + { + indent(sb, level, "pub fn %s_at_most_%d_items_from_slice(&mut self, value: &[%s]) {\n", + formatFunctionName(name), arrayLength, rustPrimitiveType); + } + else + { + indent(sb, level, "pub fn %s_at_most_%d_items_from_slice_padded_with_zero(&mut self, value: &[%s]) {\n", + formatFunctionName(name), arrayLength, rustPrimitiveType); + } + + // NB: must create variable 'offset' before calling mutable self.get_buf_mut() + indent(sb, level + 1, "let offset = self.%s;\n", getBufOffset(typeToken)); + indent(sb, level + 1, "let buf = self.get_buf_mut();\n"); + indent(sb, level + 1, "let len = value.len();\n"); + + if (rustPrimitiveType.equals("u8")) + { + generatePrimitiveArrayEncoderWithSliceRefMethodBodyForU8( + sb, level, typeToken, name, rustPrimitiveType, withPadding); + } + else + { + generatePrimitiveArrayEncoderWithSliceRefMethodBodyForNonU8( + sb, level, typeToken, name, primitiveType, rustPrimitiveType, withPadding); + } + + indent(sb, level, "}\n\n"); + } + private static void generateEnumEncoder( final StringBuilder sb, final int level, diff --git a/sbe-tool/src/test/resources/fixed-sized-primitive-array-types.xml b/sbe-tool/src/test/resources/fixed-sized-primitive-array-types.xml new file mode 100644 index 0000000000..62f3b9a5b0 --- /dev/null +++ b/sbe-tool/src/test/resources/fixed-sized-primitive-array-types.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +