use crate::dialect::{Dialect, effective_min_count};
use crate::feature_flags::{Feature, FeatureFlags};
use crate::{Error, ErrorCode, Field, FieldKind, Occurs, Result, Schema, TailODO, error::error};
use std::collections::HashMap;
use tracing::debug;
const MAX_RECORD_SIZE: u64 = 16 * 1024 * 1024;
#[derive(Debug)]
struct LayoutContext {
current_offset: u64,
redefines_clusters: HashMap<String, (u64, u64)>,
odo_arrays: Vec<OdoInfo>,
field_paths: HashMap<String, u64>, }
#[derive(Debug, Clone)]
struct OdoInfo {
array_path: String,
counter_path: String,
array_offset: u64,
max_count: u32,
min_count: u32,
}
impl LayoutContext {
fn new() -> Self {
Self {
current_offset: 0,
redefines_clusters: HashMap::new(),
odo_arrays: Vec::new(),
field_paths: HashMap::new(),
}
}
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn resolve_layout(schema: &mut Schema, dialect: Dialect) -> Result<()> {
let mut context = LayoutContext::new();
collect_redefines_info(&schema.fields, &mut context);
for field in &mut schema.fields {
resolve_field_layout(field, &mut context, None)?;
}
validate_odo_constraints(&context)?;
calculate_fixed_record_length(schema, &context)?;
detect_tail_odo(schema, &context, dialect);
resolve_renames_aliases(&mut schema.fields)?;
if context.current_offset > MAX_RECORD_SIZE {
return Err(error!(
ErrorCode::CBKS141_RECORD_TOO_LARGE,
"Theoretical maximum record size {} bytes exceeds limit of {} bytes",
context.current_offset,
MAX_RECORD_SIZE
));
}
Ok(())
}
fn collect_redefines_info(fields: &[Field], context: &mut LayoutContext) {
for field in fields {
if let Some(target) = field.redefines_of.as_ref() {
if !context.redefines_clusters.contains_key(target) {
context.redefines_clusters.insert(target.clone(), (0, 0));
}
}
collect_redefines_info(&field.children, context);
}
}
fn resolve_field_layout(
field: &mut Field,
context: &mut LayoutContext,
parent_path: Option<&str>,
) -> Result<u64> {
let field_path = if let Some(parent) = parent_path {
format!("{}.{}", parent, field.name)
} else {
field.name.clone()
};
field.path.clone_from(&field_path);
if let Some(target) = field.redefines_of.clone() {
return resolve_redefines_field(field, context, &target, &field_path);
}
let (alignment, base_size) =
calculate_field_size_and_alignment(&field.kind, field.synchronized);
for (cluster_start, cluster_size) in context.redefines_clusters.values() {
let cluster_end = cluster_start + cluster_size;
context.current_offset = context.current_offset.max(cluster_end);
}
let aligned_offset = apply_alignment(context.current_offset, alignment);
let padding_bytes = aligned_offset - context.current_offset;
if padding_bytes > 0 {
field.sync_padding = Some(copybook_overflow::safe_u64_to_u16(
padding_bytes,
"sync padding calculation",
)?);
}
field.offset = copybook_overflow::safe_u64_to_u32(aligned_offset, "field offset calculation")?;
context.current_offset = aligned_offset;
context
.field_paths
.insert(field_path.clone(), aligned_offset);
let cluster_key = field.name.clone();
if let Some((cluster_start, current_max)) =
context.redefines_clusters.get(&cluster_key).copied()
{
let new_cluster_start = if cluster_start == 0 {
aligned_offset
} else {
cluster_start
};
let field_effective_size = match &field.occurs {
Some(Occurs::Fixed { count }) => u64::from(base_size) * u64::from(*count),
Some(Occurs::ODO { max, .. }) => u64::from(base_size) * u64::from(*max),
None => u64::from(base_size),
};
context.redefines_clusters.insert(
cluster_key,
(new_cluster_start, current_max.max(field_effective_size)),
);
}
let effective_size = match &field.occurs {
Some(Occurs::Fixed { count }) => {
let base_u64 = u64::from(base_size);
let count_u64 = u64::from(*count);
if count_u64 <= 1000 && base_u64 <= 1000 {
base_u64 * count_u64
} else {
base_u64.checked_mul(count_u64).ok_or_else(|| {
error!(
ErrorCode::CBKS141_RECORD_TOO_LARGE,
"Fixed array size overflow for field '{}'", field.name
)
})?
}
}
Some(Occurs::ODO {
min,
max,
counter_path,
}) => {
let base_u64 = u64::from(base_size);
let max_u64 = u64::from(*max);
let size = if max_u64 <= 1000 && base_u64 <= 1000 {
base_u64 * max_u64
} else {
base_u64.checked_mul(max_u64).ok_or_else(|| {
error!(
ErrorCode::CBKS141_RECORD_TOO_LARGE,
"ODO array size overflow for field '{}'", field.name
)
})?
};
context.odo_arrays.push(OdoInfo {
array_path: field_path.clone(),
counter_path: counter_path.clone(),
array_offset: aligned_offset,
max_count: *max,
min_count: *min,
});
size
}
None => u64::from(base_size), };
if matches!(field.kind, FieldKind::Group) {
let mut group_size = 0u64;
let group_start_offset = context.current_offset;
for child in &mut field.children {
let child_end_offset = resolve_field_layout(child, context, Some(&field_path))?;
group_size = group_size.max(child_end_offset - group_start_offset);
}
field.len =
copybook_overflow::safe_u64_to_u32(group_size, "group field length calculation")?;
let group_effective_size = match &field.occurs {
Some(Occurs::Fixed { count }) => {
let count_u64 = u64::from(*count);
if count_u64 <= 1000 && group_size <= 1000 {
group_size * count_u64
} else {
group_size.checked_mul(count_u64).ok_or_else(|| {
error!(
ErrorCode::CBKS141_RECORD_TOO_LARGE,
"Group OCCURS size overflow for field '{}'", field.name
)
})?
}
}
Some(Occurs::ODO { max, .. }) => {
let max_u64 = u64::from(*max);
if max_u64 <= 1000 && group_size <= 1000 {
group_size * max_u64
} else {
group_size.checked_mul(max_u64).ok_or_else(|| {
error!(
ErrorCode::CBKS141_RECORD_TOO_LARGE,
"Group ODO size overflow for field '{}'", field.name
)
})?
}
}
None => group_size,
};
let final_offset = group_start_offset + group_effective_size;
context.current_offset = final_offset;
Ok(final_offset)
} else {
field.len = base_size;
let final_offset = context.current_offset + effective_size;
context.current_offset = final_offset;
Ok(final_offset)
}
}
fn resolve_redefines_field(
field: &mut Field,
context: &mut LayoutContext,
target: &str,
field_path: &str,
) -> Result<u64> {
let target_offset = context
.field_paths
.get(target)
.or_else(|| {
context
.field_paths
.iter()
.find(|(path, _)| path.ends_with(&format!(".{}", target)) || path == &target)
.map(|(_, offset)| offset)
})
.copied()
.ok_or_else(|| {
error!(
ErrorCode::CBKP001_SYNTAX,
"REDEFINES target '{}' not found for field '{}'", target, field.name
)
})?;
let (alignment, base_size) =
calculate_field_size_and_alignment(&field.kind, field.synchronized);
let aligned_offset = apply_alignment(target_offset, alignment);
let padding_bytes = aligned_offset - target_offset;
if padding_bytes > 0 {
field.sync_padding = Some(copybook_overflow::safe_u64_to_u16(
padding_bytes,
"redefines sync padding calculation",
)?);
}
field.offset =
copybook_overflow::safe_u64_to_u32(aligned_offset, "redefines field offset calculation")?;
let effective_size = match &field.occurs {
Some(Occurs::Fixed { count }) => u64::from(base_size)
.checked_mul(u64::from(*count))
.ok_or_else(|| {
error!(
ErrorCode::CBKS141_RECORD_TOO_LARGE,
"REDEFINES array size overflow for field '{}'", field.name
)
})?,
Some(Occurs::ODO { max, .. }) => u64::from(base_size)
.checked_mul(u64::from(*max))
.ok_or_else(|| {
error!(
ErrorCode::CBKS141_RECORD_TOO_LARGE,
"REDEFINES ODO array size overflow for field '{}'", field.name
)
})?,
None => u64::from(base_size),
};
if matches!(field.kind, FieldKind::Group) {
let mut group_size = 0u64;
let saved_offset = context.current_offset;
context.current_offset = aligned_offset;
for child in &mut field.children {
let child_end_offset = resolve_field_layout(child, context, Some(field_path))?;
group_size = group_size.max(child_end_offset - aligned_offset);
}
field.len = copybook_overflow::safe_u64_to_u32(
group_size,
"redefines group field length calculation",
)?;
context.current_offset = saved_offset;
let group_effective_size = match &field.occurs {
Some(Occurs::Fixed { count }) => {
let count_u64 = u64::from(*count);
group_size.checked_mul(count_u64).ok_or_else(|| {
error!(
ErrorCode::CBKS141_RECORD_TOO_LARGE,
"REDEFINES group OCCURS size overflow for field '{}'", field.name
)
})?
}
Some(Occurs::ODO { max, .. }) => {
let max_u64 = u64::from(*max);
group_size.checked_mul(max_u64).ok_or_else(|| {
error!(
ErrorCode::CBKS141_RECORD_TOO_LARGE,
"REDEFINES group ODO size overflow for field '{}'", field.name
)
})?
}
None => group_size,
};
let cluster_key = target.to_string();
let (cluster_start, current_max) = context
.redefines_clusters
.get(&cluster_key)
.copied()
.unwrap_or((target_offset, 0));
let new_max = current_max.max(group_effective_size);
context
.redefines_clusters
.insert(cluster_key, (cluster_start, new_max));
Ok(aligned_offset + group_effective_size)
} else {
field.len = base_size;
let cluster_key = target.to_string();
let (cluster_start, current_max) = context
.redefines_clusters
.get(&cluster_key)
.copied()
.unwrap_or((target_offset, 0u64));
let new_max = current_max.max(effective_size);
context
.redefines_clusters
.insert(cluster_key, (cluster_start, new_max));
context
.field_paths
.insert(field_path.to_string(), aligned_offset);
Ok(aligned_offset + effective_size)
}
}
fn calculate_field_size_and_alignment(kind: &FieldKind, synchronized: bool) -> (u64, u32) {
let (size, natural_alignment) = match kind {
FieldKind::Alphanum { len } => (*len, 1u64),
FieldKind::ZonedDecimal {
digits,
sign_separate,
..
} => {
let base_size = u32::from(*digits);
let size = if sign_separate.is_some() {
base_size + 1 } else {
base_size
};
(size, 1u64)
}
FieldKind::BinaryInt { bits, .. } => {
let bytes = u32::from(*bits) / 8;
let alignment = if synchronized { u64::from(bytes) } else { 1u64 };
(bytes, alignment)
}
FieldKind::PackedDecimal { digits, .. } => {
#[allow(clippy::manual_midpoint)] let bytes = (u32::from(*digits) + 2) / 2; (bytes, 1u64)
}
FieldKind::Group => (0, 1u64), FieldKind::Condition { .. } => (0, 1u64), FieldKind::Renames { .. } => (0, 1u64), FieldKind::EditedNumeric { width, .. } => (u32::from(*width), 1u64), FieldKind::FloatSingle => (4, if synchronized { 4u64 } else { 1u64 }),
FieldKind::FloatDouble => (8, if synchronized { 8u64 } else { 1u64 }),
};
let alignment = if synchronized && natural_alignment > 1 {
natural_alignment
} else {
1u64
};
(alignment, size)
}
fn apply_alignment(offset: u64, alignment: u64) -> u64 {
if alignment <= 1 {
offset
} else {
offset.div_ceil(alignment) * alignment
}
}
fn validate_odo_constraints(context: &LayoutContext) -> Result<()> {
for odo in &context.odo_arrays {
let counter_offset = context
.field_paths
.get(&odo.counter_path)
.or_else(|| {
context
.field_paths
.iter()
.find(|(path, _)| {
path.ends_with(&format!(".{}", odo.counter_path))
|| path == &&odo.counter_path
})
.map(|(_, offset)| offset)
})
.copied()
.ok_or_else(|| {
Error::new(
ErrorCode::CBKS121_COUNTER_NOT_FOUND,
format!(
"ODO counter field '{}' not found for array '{}'",
odo.counter_path, odo.array_path
),
)
.with_context(crate::error::ErrorContext {
record_index: None,
field_path: Some(odo.array_path.clone()),
byte_offset: Some(odo.array_offset),
line_number: None,
details: Some(format!(
"counter_field={}, searched_paths=[{}]",
odo.counter_path,
context
.field_paths
.keys()
.map(std::string::String::as_str)
.collect::<Vec<_>>()
.join(", ")
)),
})
})?;
if counter_offset >= odo.array_offset {
return Err(Error::new(
ErrorCode::CBKS121_COUNTER_NOT_FOUND,
format!(
"ODO counter '{}' (offset {}) must precede array '{}' (offset {}) in byte order",
odo.counter_path, counter_offset, odo.array_path, odo.array_offset
)
).with_context(crate::error::ErrorContext {
record_index: None,
field_path: Some(odo.array_path.clone()),
byte_offset: Some(odo.array_offset),
line_number: None,
details: Some(format!("counter_offset={}, array_offset={}, min_count={}, max_count={}",
counter_offset, odo.array_offset, odo.min_count, odo.max_count)),
}));
}
debug!(
"ODO validation passed: array='{}', counter='{}', array_offset={}, counter_offset={}",
odo.array_path, odo.counter_path, odo.array_offset, counter_offset
);
}
Ok(())
}
fn calculate_fixed_record_length(schema: &mut Schema, context: &LayoutContext) -> Result<()> {
let mut total_size = context.current_offset;
for (cluster_start, cluster_size) in context.redefines_clusters.values() {
let cluster_end = cluster_start + cluster_size;
total_size = total_size.max(cluster_end);
}
schema.lrecl_fixed = Some(copybook_overflow::safe_u64_to_u32(
total_size,
"fixed record length calculation",
)?);
Ok(())
}
fn detect_tail_odo(schema: &mut Schema, context: &LayoutContext, dialect: Dialect) {
if let Some(tail_odo) = context.odo_arrays.iter().max_by_key(|odo| odo.array_offset) {
let array_field_name = tail_odo
.array_path
.split('.')
.next_back()
.unwrap_or(&tail_odo.array_path)
.to_string();
let counter_field_name = tail_odo
.counter_path
.split('.')
.next_back()
.unwrap_or(&tail_odo.counter_path)
.to_string();
let effective_min = effective_min_count(dialect, tail_odo.min_count);
schema.tail_odo = Some(TailODO {
counter_path: counter_field_name,
min_count: effective_min,
max_count: tail_odo.max_count,
array_path: array_field_name,
});
}
}
fn head_ident_of_qname(q: &str) -> &str {
q.split_whitespace().next().unwrap_or(q)
}
fn find_sibling_index_by_qname(siblings: &[crate::schema::Field], qname: &str) -> Option<usize> {
let needle = head_ident_of_qname(qname).trim();
siblings
.iter()
.enumerate()
.filter(|(_, f)| f.level != 66 && f.level != 88) .find(|(_, f)| f.name.trim().eq_ignore_ascii_case(needle))
.map(|(i, _)| i)
}
fn find_field_by_name<'a>(
fields: &'a [crate::schema::Field],
name: &str,
) -> Option<&'a crate::schema::Field> {
let needle = head_ident_of_qname(name).trim();
for field in fields {
if field.level != 66 && field.level != 88 && field.name.trim().eq_ignore_ascii_case(needle)
{
return Some(field);
}
if !field.children.is_empty()
&& let Some(found) = find_field_by_name(&field.children, name)
{
return Some(found);
}
}
None
}
fn collect_storage_paths(field: &crate::schema::Field, paths: &mut Vec<String>) {
if !matches!(field.kind, crate::schema::FieldKind::Group) {
paths.push(field.path.clone());
}
for child in &field.children {
if child.level != 66 && child.level != 88 {
collect_storage_paths(child, paths);
}
}
}
fn field_is_redefines(field: &crate::schema::Field) -> bool {
field.redefines_of.is_some()
}
fn field_has_redefines(field: &crate::schema::Field) -> bool {
if field_is_redefines(field) {
return true;
}
for child in &field.children {
if field_has_redefines(child) {
return true;
}
}
false
}
fn field_is_occurs(field: &crate::schema::Field) -> bool {
matches!(
field.occurs,
Some(Occurs::Fixed { .. } | Occurs::ODO { .. })
)
}
fn field_is_odo(field: &crate::schema::Field) -> bool {
matches!(field.occurs, Some(Occurs::ODO { .. }))
}
fn count_redefines_alternatives(
fields: &[crate::schema::Field],
from_idx: usize,
thru_idx: usize,
) -> usize {
let mut count = 0;
for f in &fields[from_idx..=thru_idx] {
if field_has_redefines(f) {
count += 1;
}
}
count
}
fn resolve_renames_aliases(fields: &mut [crate::schema::Field]) -> Result<()> {
use crate::error::ErrorCode;
use crate::schema::{FieldKind, ResolvedRenames};
let r4_r6_enabled = FeatureFlags::global().is_enabled(Feature::RenamesR4R6);
for f in fields.iter_mut() {
if !f.children.is_empty() {
resolve_renames_aliases(&mut f.children)?;
}
}
let mut resolutions: Vec<(usize, u32, u32, Vec<String>)> = Vec::new();
for (idx, field) in fields.iter().enumerate() {
if let FieldKind::Renames {
ref from_field,
ref thru_field,
} = field.kind
{
let from_i_opt = find_sibling_index_by_qname(fields, from_field);
let thru_i_opt = find_sibling_index_by_qname(fields, thru_field);
let is_nested_single_group =
from_i_opt.is_none() && thru_i_opt.is_none() && from_field == thru_field;
if is_nested_single_group {
let target_field = find_field_by_name(fields, from_field).ok_or_else(|| {
error!(
ErrorCode::CBKS601_RENAME_UNKNOWN_FROM,
"RENAMES nested target field '{}' not found", from_field
)
})?;
let offset = target_field.offset;
let length = target_field.len;
let mut members = Vec::new();
collect_storage_paths(target_field, &mut members);
resolutions.push((idx, offset, length, members));
continue; }
let from_i = from_i_opt.ok_or_else(|| {
error!(
ErrorCode::CBKS601_RENAME_UNKNOWN_FROM,
"RENAMES from field '{}' not found", from_field
)
})?;
let thru_i = thru_i_opt.ok_or_else(|| {
error!(
ErrorCode::CBKS602_RENAME_UNKNOWN_THRU,
"RENAMES thru field '{}' not found", thru_field
)
})?;
if from_i > thru_i {
return Err(error!(
ErrorCode::CBKS604_RENAME_REVERSED_RANGE,
"RENAMES from '{}' comes after thru '{}'", from_field, thru_field
));
}
for f in &fields[from_i..=thru_i] {
if field_is_occurs(f) {
if field_is_odo(f) {
return Err(error!(
ErrorCode::CBKS612_RENAME_ODO_NOT_SUPPORTED,
"RENAMES alias '{}' spans ODO array '{}'. This pattern is not supported.",
field.name,
f.name
));
}
if !r4_r6_enabled {
return Err(error!(
ErrorCode::CBKS607_RENAME_CROSSES_OCCURS,
"RENAMES alias '{}' crosses OCCURS boundary at field '{}'. Enable RenamesR4R6 feature flag to support this pattern.",
field.name,
f.name
));
}
if from_i != thru_i {
return Err(error!(
ErrorCode::CBKS611_RENAME_PARTIAL_OCCURS,
"RENAMES alias '{}' spans partial array elements. Only single-array RENAMES (FROM==THRU pointing to OCCURS field) is supported.",
field.name
));
}
}
}
if r4_r6_enabled {
let redefines_count = count_redefines_alternatives(fields, from_i, thru_i);
if redefines_count > 1 {
return Err(error!(
ErrorCode::CBKS610_RENAME_MULTIPLE_REDEFINES,
"RENAMES alias '{}' spans multiple REDEFINES alternatives. Only single-alternative RENAMES is supported.",
field.name
));
}
} else {
let redefines_count = count_redefines_alternatives(fields, from_i, thru_i);
if redefines_count > 0 {
return Err(error!(
ErrorCode::CBKS609_RENAME_OVER_REDEFINES,
"RENAMES alias '{}' spans REDEFINES field(s). Enable RenamesR4R6 feature flag to support this pattern.",
field.name
));
}
}
let has_storage_children = |field: &Field| {
field
.children
.iter()
.any(|child| child.level != 88 && child.level != 66)
};
let is_single_group_rename = from_i == thru_i && has_storage_children(&fields[from_i]);
if !is_single_group_rename {
if has_storage_children(&fields[from_i]) {
return Err(error!(
ErrorCode::CBKS605_RENAME_FROM_CROSSES_GROUP,
"RENAMES from field '{}' is a group with storage-bearing children; cannot span groups",
from_field
));
}
if has_storage_children(&fields[thru_i]) {
return Err(error!(
ErrorCode::CBKS606_RENAME_THRU_CROSSES_GROUP,
"RENAMES thru field '{}' is a group with storage-bearing children; cannot span groups",
thru_field
));
}
if from_i + 1 < thru_i {
for f in &fields[(from_i + 1)..thru_i] {
if has_storage_children(f) {
return Err(error!(
ErrorCode::CBKS605_RENAME_FROM_CROSSES_GROUP,
"RENAMES range from '{}' to '{}' crosses group boundary at field '{}'",
from_field,
thru_field,
f.name
));
}
}
}
}
let offset = fields[from_i].offset;
let end_offset_u64 = (fields[thru_i].offset as u64) + (fields[thru_i].len as u64);
let length: u32 = (end_offset_u64 - (offset as u64)).try_into().map_err(|_| {
error!(
ErrorCode::CBKS141_RECORD_TOO_LARGE,
"RENAMES alias '{}' exceeds maximum size", field.name
)
})?;
let mut members = Vec::new();
if is_single_group_rename {
collect_storage_paths(&fields[from_i], &mut members);
} else {
for field in &fields[from_i..=thru_i] {
let lvl = field.level;
if lvl != 66 && lvl != 88 {
members.push(field.path.clone());
}
}
}
resolutions.push((idx, offset, length, members));
}
}
for (idx, offset, length, members) in resolutions {
fields[idx].resolved_renames = Some(ResolvedRenames {
offset,
length,
members,
});
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::expect_used)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::schema::{Field, FieldKind, Occurs};
const TEST_DIALECT: Dialect = Dialect::Normative;
#[test]
fn test_simple_field_layout() {
let mut schema = Schema::new();
let field = Field::with_kind(1, "TEST-FIELD".to_string(), FieldKind::Alphanum { len: 10 });
schema.fields.push(field);
resolve_layout(&mut schema, TEST_DIALECT).unwrap();
let field = &schema.fields[0];
assert_eq!(field.offset, 0);
assert_eq!(field.len, 10);
assert_eq!(schema.lrecl_fixed, Some(10));
}
#[test]
fn test_binary_field_alignment() {
let mut schema = Schema::new();
let char_field =
Field::with_kind(1, "CHAR-FIELD".to_string(), FieldKind::Alphanum { len: 1 });
schema.fields.push(char_field);
let mut binary_field = Field::with_kind(
1,
"BINARY-FIELD".to_string(),
FieldKind::BinaryInt {
bits: 32,
signed: false,
},
);
binary_field.synchronized = true;
schema.fields.push(binary_field);
resolve_layout(&mut schema, TEST_DIALECT).unwrap();
let char_field = &schema.fields[0];
assert_eq!(char_field.offset, 0);
assert_eq!(char_field.len, 1);
let binary_field = &schema.fields[1];
assert_eq!(binary_field.offset, 4); assert_eq!(binary_field.len, 4);
assert_eq!(binary_field.sync_padding, Some(3)); }
#[test]
fn test_occurs_fixed_array() {
let mut schema = Schema::new();
let mut field =
Field::with_kind(1, "ARRAY-FIELD".to_string(), FieldKind::Alphanum { len: 5 });
field.occurs = Some(Occurs::Fixed { count: 10 });
schema.fields.push(field);
resolve_layout(&mut schema, TEST_DIALECT).unwrap();
let field = &schema.fields[0];
assert_eq!(field.offset, 0);
assert_eq!(field.len, 5); assert_eq!(schema.lrecl_fixed, Some(50)); }
#[test]
fn test_odo_array() {
let mut schema = Schema::new();
let counter = Field::with_kind(
1,
"COUNTER".to_string(),
FieldKind::ZonedDecimal {
digits: 3,
scale: 0,
signed: false,
sign_separate: None,
},
);
schema.fields.push(counter);
let mut array_field = Field::with_kind(
1,
"ARRAY-FIELD".to_string(),
FieldKind::Alphanum { len: 10 },
);
array_field.occurs = Some(Occurs::ODO {
min: 0,
max: 5,
counter_path: "COUNTER".to_string(),
});
schema.fields.push(array_field);
resolve_layout(&mut schema, TEST_DIALECT).unwrap();
let counter = &schema.fields[0];
assert_eq!(counter.offset, 0);
assert_eq!(counter.len, 3);
let array_field = &schema.fields[1];
assert_eq!(array_field.offset, 3);
assert_eq!(array_field.len, 10);
assert!(schema.tail_odo.is_some());
let tail_odo = schema.tail_odo.as_ref().unwrap();
assert_eq!(tail_odo.counter_path, "COUNTER");
assert_eq!(tail_odo.max_count, 5);
assert_eq!(schema.lrecl_fixed, Some(53));
}
#[test]
fn test_redefines_cluster_sizing() {
let mut schema = Schema::new();
let field_a = Field::with_kind(1, "FIELD-A".to_string(), FieldKind::Alphanum { len: 10 });
schema.fields.push(field_a);
let mut field_b =
Field::with_kind(1, "FIELD-B".to_string(), FieldKind::Alphanum { len: 5 });
field_b.redefines_of = Some("FIELD-A".to_string());
schema.fields.push(field_b);
let mut field_c =
Field::with_kind(1, "FIELD-C".to_string(), FieldKind::Alphanum { len: 15 });
field_c.redefines_of = Some("FIELD-A".to_string());
schema.fields.push(field_c);
resolve_layout(&mut schema, TEST_DIALECT).unwrap();
let field_a = &schema.fields[0];
assert_eq!(field_a.offset, 0);
assert_eq!(field_a.len, 10);
let field_b = &schema.fields[1];
assert_eq!(field_b.offset, 0); assert_eq!(field_b.len, 5);
let field_c = &schema.fields[2];
assert_eq!(field_c.offset, 0); assert_eq!(field_c.len, 15);
assert_eq!(schema.lrecl_fixed, Some(15));
}
#[test]
fn test_record_size_overflow() {
let mut schema = Schema::new();
let mut huge_field = Field::with_kind(
1,
"HUGE-FIELD".to_string(),
FieldKind::Alphanum { len: u32::MAX },
);
huge_field.occurs = Some(Occurs::Fixed { count: 1000 });
schema.fields.push(huge_field);
let result = resolve_layout(&mut schema, TEST_DIALECT);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err().code,
ErrorCode::CBKS141_RECORD_TOO_LARGE
));
}
#[test]
fn test_odo_counter_validation() {
let mut schema = Schema::new();
let mut array_field = Field::with_kind(
1,
"ARRAY-FIELD".to_string(),
FieldKind::Alphanum { len: 10 },
);
array_field.occurs = Some(Occurs::ODO {
min: 0,
max: 5,
counter_path: "NONEXISTENT".to_string(),
});
schema.fields.push(array_field);
let result = resolve_layout(&mut schema, TEST_DIALECT);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err().code,
ErrorCode::CBKS121_COUNTER_NOT_FOUND
));
}
#[test]
fn test_packed_decimal_size() {
let mut schema = Schema::new();
let field = Field::with_kind(
1,
"PACKED-FIELD".to_string(),
FieldKind::PackedDecimal {
digits: 7,
scale: 2,
signed: true,
},
);
schema.fields.push(field);
resolve_layout(&mut schema, TEST_DIALECT).unwrap();
let field = &schema.fields[0];
assert_eq!(field.len, 4);
}
#[test]
fn test_binary_width_mapping() {
let mut schema = Schema::new();
let test_cases = vec![
(4, 16), (9, 32), (18, 64), ];
for (digits, expected_bits) in test_cases {
let field = Field::with_kind(
1,
format!("BINARY-{}", digits),
FieldKind::BinaryInt {
bits: expected_bits,
signed: false,
},
);
schema.fields.push(field);
}
resolve_layout(&mut schema, TEST_DIALECT).unwrap();
assert_eq!(schema.fields[0].len, 2); assert_eq!(schema.fields[1].len, 4); assert_eq!(schema.fields[2].len, 8); }
}