diff --git a/examples/demo3270.rs b/examples/demo3270.rs index 7b1ec75..60d99cd 100644 --- a/examples/demo3270.rs +++ b/examples/demo3270.rs @@ -36,17 +36,19 @@ fn run(mut session: tn3270::Session) -> anyhow::Result<()> { WriteOrder::SetBufferAddress(0), WriteOrder::EraseUnprotectedToAddress(bufsz.last_address()), WriteOrder::SetBufferAddress(bufsz.encode_address(1, 31)), - WriteOrder::StartFieldExtended(FieldAttribute::PROTECTED, vec![ + WriteOrder::StartFieldExtended(vec![ + ExtendedFieldAttribute::FieldAttribute(FieldAttribute::PROTECTED), // ExtendedFieldAttribute::ForegroundColor(Color::Red), ]), WriteOrder::SendText("Hello from Rust!".into()), WriteOrder::SetBufferAddress(bufsz.encode_address(8, 21)), - WriteOrder::StartFieldExtended(FieldAttribute::INTENSE_SELECTOR_PEN_DETECTABLE, vec![]), + WriteOrder::StartField(FieldAttribute::INTENSE_SELECTOR_PEN_DETECTABLE), WriteOrder::SendText(" ".into()), WriteOrder::StartField(FieldAttribute::PROTECTED), WriteOrder::SetBufferAddress(bufsz.encode_address(8, 10)), - WriteOrder::StartFieldExtended(FieldAttribute::PROTECTED, vec![ - ExtendedFieldAttribute::ForegroundColor(Color::Turquoise), + WriteOrder::StartFieldExtended(vec![ + ExtendedFieldAttribute::FieldAttribute(FieldAttribute::PROTECTED), + // ExtendedFieldAttribute::ForegroundColor(Color::Turquoise), ]), WriteOrder::SendText("Name:".into()), ], @@ -54,7 +56,8 @@ fn run(mut session: tn3270::Session) -> anyhow::Result<()> { for (i, line) in rust_logo.iter().enumerate() { record.orders.push(WriteOrder::SetBufferAddress(bufsz.encode_address(3+i as u16, 31))); - record.orders.push(WriteOrder::StartFieldExtended(FieldAttribute::PROTECTED, vec![ + record.orders.push(WriteOrder::StartFieldExtended(vec![ + ExtendedFieldAttribute::FieldAttribute(FieldAttribute::PROTECTED), ExtendedFieldAttribute::ForegroundColor(Color::Red), ])); record.orders.push(WriteOrder::SendText((*line).into())); @@ -68,13 +71,25 @@ fn run(mut session: tn3270::Session) -> anyhow::Result<()> { let record = session.receive_record(None)?; if let Some(record) = record { - eprintln!("Incoming record: {:?}", hex::encode(record)); + eprintln!("Incoming record: {:?}", hex::encode(&record)); + eprintln!("Decoded: {:#?}", IncomingRecord::parse_record(record.as_slice())) } else { eprintln!("No record"); } + session.send_record(&WriteCommand{ + command: WriteCommandCode::Write, + wcc: WCC::RESET_MDT, + orders: vec![ + WriteOrder::SetBufferAddress(bufsz.encode_address(8,21)), + WriteOrder::ModifyField(vec![ + ExtendedFieldAttribute::FieldAttribute(FieldAttribute::PROTECTED), + ]), + ], + })?; - std::thread::sleep(Duration::from_secs(5)); + + std::thread::sleep(Duration::from_secs(50)); Ok(()) } diff --git a/src/tn3270/stream.rs b/src/tn3270/stream.rs index faf6977..4d8fa4c 100644 --- a/src/tn3270/stream.rs +++ b/src/tn3270/stream.rs @@ -1,12 +1,18 @@ use bitflags::bitflags; use std::io::Write; -use std::convert::TryFrom; -use snafu::Snafu; +use std::convert::{TryFrom, TryInto}; +use snafu::{Snafu, ensure}; +use hex::encode; +use std::fs::read_to_string; #[derive(Clone, Debug, Snafu)] pub enum StreamFormatError { #[snafu(display("Invalid AID: {:02x}", aid))] - InvalidAID { aid: u8, } + InvalidAID { aid: u8, }, + #[snafu(display("Record ended early"))] + UnexpectedEOR, + #[snafu(display("Invalid data"))] + InvalidData, } const WCC_TRANS: [u8; 64] = [ @@ -35,12 +41,15 @@ bitflags! { bitflags! { pub struct FieldAttribute: u8 { + const HI_1 = 0x40; + const HI_2 = 0x80; const PROTECTED = 0x20; const NUMERIC = 0x10; const NON_DISPLAY = 0x0C; const DISPLAY_SELECTOR_PEN_DETECTABLE = 0x04; const INTENSE_SELECTOR_PEN_DETECTABLE = 0x08; const MODIFIED = 0x01; + const NONE = 0x00; } } @@ -138,24 +147,60 @@ impl Into for Color { } } +impl TryFrom for Color { + type Error = StreamFormatError; + + fn try_from(value: u8) -> Result { + Ok(match value { + 0x00 => Color::Default, + 0xF0 => Color::NeutralBG, + 0xF1 => Color::Blue, + 0xF2 => Color::Red, + 0xF3 => Color::Pink, + 0xF4 => Color::Green, + 0xF5 => Color::Turquoise, + 0xF6 => Color::Yellow, + 0xF7 => Color::NeutralFG, + 0xF8 => Color::Black, + 0xF9 => Color::DeepBlue, + 0xFA => Color::Orange, + 0xFB => Color::Purple, + 0xFC => Color::PaleGreen, + 0xFD => Color::PaleTurquoise, + 0xFE => Color::Grey, + 0xFF => Color::White, + _ => return Err(StreamFormatError::InvalidData), + }) + } +} + #[derive(Copy, Clone, Debug, Hash)] pub enum Highlighting { - Default, - Normal, - Blink, - Reverse, - Underscore, + Default = 0x00, + Normal = 0xF0, + Blink = 0xF1, + Reverse = 0xF2, + Underscore = 0xF4, +} + +impl TryFrom for Highlighting { + type Error = StreamFormatError; + + fn try_from(v: u8) -> Result { + Ok(match v { + 0x00 => Highlighting::Default, + 0xF0 => Highlighting::Normal, + 0xF1 => Highlighting::Blink, + 0xF2 => Highlighting::Reverse, + 0xF4 => Highlighting::Underscore, + _ => return Err(StreamFormatError::InvalidData) + }) + } } impl Into for Highlighting { fn into(self) -> u8 { - match self { - Highlighting::Default => 0x00, - Highlighting::Normal => 0xF0, - Highlighting::Blink => 0xF1, - Highlighting::Reverse => 0xF2, - Highlighting::Underscore => 0xF4, - } + self as u8 } } @@ -177,9 +222,9 @@ pub enum Transparency { Opaque, } -impl Into for Transparency { - fn into(self) -> u8 { - match self { +impl From for u8 { + fn from(v: Transparency) -> u8 { + match v { Transparency::Default => 0x00, Transparency::Or => 0xF0, Transparency::Xor => 0xF1, @@ -188,6 +233,20 @@ impl Into for Transparency { } } +impl TryFrom for Transparency { + type Error = StreamFormatError; + + fn try_from(value: u8) -> Result { + Ok(match value { + 0x00 => Transparency::Default, + 0xF0 => Transparency::Or, + 0xF1 => Transparency::Xor, + 0xF2 => Transparency::Opaque, + _ => return Err(StreamFormatError::InvalidData) + }) + } +} + bitflags! { pub struct FieldValidation: u8 { const MANDATORY_FILL = 0b100; @@ -207,7 +266,27 @@ pub enum ExtendedFieldAttribute { FieldAttribute(FieldAttribute), FieldValidation(FieldValidation), FieldOutlining(FieldOutline), +} +impl TryFrom<&[u8]> for ExtendedFieldAttribute { + type Error = StreamFormatError; + + fn try_from(value: &[u8]) -> Result { + ensure!(value.len() == 2, UnexpectedEOR); + Ok(match (value[0], value[1]) { + (0x00, 0x00) => ExtendedFieldAttribute::AllAttributes, + (0x00, _) => return Err(StreamFormatError::InvalidData), + (0xC0, fa) => ExtendedFieldAttribute::FieldAttribute(FieldAttribute::from_bits(fa & 0x3F).ok_or(StreamFormatError::InvalidData)?), + (0x41, v) => ExtendedFieldAttribute::ExtendedHighlighting(v.try_into()?), + (0x45, v) => ExtendedFieldAttribute::BackgroundColor(v.try_into()?), + (0x42, v) => ExtendedFieldAttribute::ForegroundColor(v.try_into()?), + (0x43, v) => ExtendedFieldAttribute::CharacterSet(v), + (0xC2, v) => ExtendedFieldAttribute::FieldOutlining(FieldOutline::from_bits(v).ok_or(StreamFormatError::InvalidData)?), + (0x46, v) => ExtendedFieldAttribute::Transparency(v.try_into()?), + (0xC1, v) => ExtendedFieldAttribute::FieldValidation(FieldValidation::from_bits(v).ok_or(StreamFormatError::InvalidData)?), + _ => return Err(StreamFormatError::InvalidData), + }) + } } impl ExtendedFieldAttribute { @@ -257,7 +336,8 @@ impl BufferAddressCalculator { #[derive(Clone, Debug)] pub enum WriteOrder { StartField(FieldAttribute), - StartFieldExtended(FieldAttribute, Vec), + /// The list of attributes MUST include a FieldAttribute + StartFieldExtended(Vec), SetBufferAddress(u16), SetAttribute(ExtendedFieldAttribute), ModifyField(Vec), @@ -274,10 +354,9 @@ impl WriteOrder { pub fn serialize(&self, output: &mut Vec) { match self { WriteOrder::StartField(attr) => output.extend_from_slice(&[0x1D, attr.bits()]), - WriteOrder::StartFieldExtended(attr, rest) => { - output.extend_from_slice(&[0x29, rest.len() as u8 + 1]); - ExtendedFieldAttribute::FieldAttribute(*attr).encode_into(&mut* output); - for attr in rest { + WriteOrder::StartFieldExtended(attrs) => { + output.extend_from_slice(&[0x29, attrs.len() as u8]); + for attr in attrs { attr.encode_into(&mut *output); } } @@ -329,6 +408,7 @@ impl Into> for &WriteCommand { } #[repr(u8)] +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] pub enum AID { NoAIDGenerated, NoAIDGeneratedPrinter, @@ -467,4 +547,135 @@ impl TryFrom for AID { _ => return Err(StreamFormatError::InvalidAID { aid }), }) } +} + +#[derive(Debug, Clone)] +pub struct IncomingRecord { + pub aid: AID, + pub addr: u16, + pub orders: Vec, +} + +fn parse_addr(encoded: &[u8]) -> Result { + match encoded[0] >> 6 { + 0b00 => Ok(((encoded[0] as u16) << 8) + encoded[1] as u16), + 0b01 | 0b11 => { + Ok((encoded[0] as u16 & 0x3F) << 6 | (encoded[1] as u16 & 0x3F)) + } + _ => Err(StreamFormatError::InvalidData), + } +} + +impl IncomingRecord { + pub fn parse_record(mut record: &[u8]) -> Result { + if record.len() < 3 { + return Err(StreamFormatError::UnexpectedEOR); + } + + let aid = AID::try_from(record[0])?; + // TODO: Handle AID 88 structured fields + let addr = parse_addr(&record[1..3])?; + + let mut result = Self { + aid, + addr, + orders: vec![] + }; + + record = &record[3..]; + + while record.len() > 0 { + match record[0] { + 0x1D => { + ensure!(record.len() >= 2, UnexpectedEOR); + result.orders.push( + WriteOrder::StartField(FieldAttribute::from_bits(record[1] & 0x3F) + .ok_or(StreamFormatError::InvalidData)?)); + record = &record[2..]; + + }, + 0x29 => { + ensure!(record.len() >= 2, UnexpectedEOR); + let (header, body) = record.split_at(2); + let count = header[1] as usize; + ensure!(body.len() >= count * 2, UnexpectedEOR); + let (attrs, rest) = body.split_at(2 * count); + record = rest; + + result.orders.push( + WriteOrder::StartFieldExtended( + attrs.chunks(2) + .map(ExtendedFieldAttribute::try_from) + .collect::, StreamFormatError>>()? + ) + ) + } + 0x11 => { + ensure!(record.len() >= 3, UnexpectedEOR); + result.orders.push(WriteOrder::SetBufferAddress(parse_addr(&record[1..3])?)); + record = &record[3..]; + } + 0x28 => { + ensure!(record.len() >= 3, UnexpectedEOR); + result.orders.push(WriteOrder::SetAttribute(ExtendedFieldAttribute::try_from(&record[1..3])?)); + record = &record[3..]; + } + 0x2C => { + ensure!(record.len() >= 2, UnexpectedEOR); + let (header, body) = record.split_at(2); + let count = header[1] as usize; + ensure!(body.len() >= count * 2, UnexpectedEOR); + let (attrs, rest) = body.split_at(2 * count); + record = rest; + + result.orders.push( + WriteOrder::ModifyField( + attrs.chunks(2) + .map(ExtendedFieldAttribute::try_from) + .collect::, StreamFormatError>>()? + ) + ) + } + 0x13 => { + ensure!(record.len() >= 3, UnexpectedEOR); + result.orders.push(WriteOrder::InsertCursor(parse_addr(&record[1..3])?)); + record = &record[3..]; + } + 0x05 => { + result.orders.push(WriteOrder::ProgramTab); + record = &record[1..]; + } + 0x3C => { + ensure!(record.len() >= 4, UnexpectedEOR); + // TODO: Handle graphic escape properly + result.orders.push(WriteOrder::RepeatToAddress( + parse_addr(&record[1..3])?, + crate::encoding::cp037::DECODE_TBL[record[4] as usize] as char, + )); + record = &record[4..] + } + 0x12 => { + ensure!(record.len() >= 3, UnexpectedEOR); + result.orders.push(WriteOrder::EraseUnprotectedToAddress(parse_addr(&record[1..3])?)); + record = &record[3..]; + } + 0x08 => { + ensure!(record.len() >= 2, UnexpectedEOR); + result.orders.push(WriteOrder::GraphicEscape(record[2])); + record = &record[2..]; + } + 0x40..=0xFF => { + let len = record.iter().position(|&v| v < 0x40).unwrap_or(record.len()); + let data = record[..len] + .iter() + .map(|&v| crate::encoding::cp037::DECODE_TBL[v as usize] as char) + .collect(); + result.orders.push(WriteOrder::SendText(data)); + record = &record[len..]; + }, + _ => return Err(StreamFormatError::InvalidData) + } + } + Ok(result) + } } \ No newline at end of file