1use zarch::ebcdic::CodePage;
6
7pub const WRITE: u8 = 0xF1;
8pub const ERASE_WRITE: u8 = 0xF5;
9pub const ERASE_WRITE_ALTERNATE: u8 = 0x7E;
10pub const ERASE_ALL_UNPROTECTED: u8 = 0x6F;
11
12pub const WCC_ALARM: u8 = 0x04;
13pub const WCC_RESTORE: u8 = 0x02;
14pub const WCC_RESET_MDT: u8 = 0x01;
15
16pub const SF: u8 = 0x1D;
17pub const SFE: u8 = 0x29;
18pub const SBA: u8 = 0x11;
19pub const SA: u8 = 0x28;
20pub const MF: u8 = 0x2C;
21pub const IC: u8 = 0x13;
22pub const PT: u8 = 0x05;
23pub const RA: u8 = 0x3C;
24pub const EUA: u8 = 0x12;
25
26pub const PROTECTED: u8 = 0x20;
27pub const NUMERIC: u8 = 0x10;
28pub const DISPLAY: u8 = 0x0C;
29pub const INTENSIFIED: u8 = 0x08;
30pub const NON_DISPLAY: u8 = 0x0C;
31pub const MDT: u8 = 0x01;
32
33pub const AID_ENTER: u8 = 0x7D;
34pub const AID_CLEAR: u8 = 0x6D;
35pub const AID_PA1: u8 = 0x6C;
36pub const AID_PA2: u8 = 0x6E;
37pub const AID_PA3: u8 = 0x6B;
38pub const AID_NONE: u8 = 0x60;
39
40const CODES: [u8; 64] = [
42 0x40, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5,
43 0xD6, 0xD7, 0xD8, 0xD9, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0x6A, 0x6B,
44 0x6C, 0x6D, 0x6E, 0x6F, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
45];
46
47pub fn encode_address(address: usize) -> [u8; 2] {
49 if address < 4096 {
50 [CODES[address >> 6], CODES[address & 0x3F]]
51 } else {
52 [((address >> 8) & 0x3F) as u8, (address & 0xFF) as u8]
53 }
54}
55
56pub fn decode_address(bytes: [u8; 2]) -> usize {
57 if bytes[0] & 0xC0 == 0 {
58 (usize::from(bytes[0] & 0x3F) << 8) | usize::from(bytes[1])
59 } else {
60 (usize::from(bytes[0] & 0x3F) << 6) | usize::from(bytes[1] & 0x3F)
61 }
62}
63
64pub fn attribute_byte(bits: u8) -> u8 {
66 CODES[usize::from(bits & 0x3F)]
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct FieldView {
72 pub attribute_at: usize,
73 pub attribute: u8,
74 pub start: usize,
75 pub len: usize,
76}
77
78#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct Screen {
80 pub rows: usize,
81 pub columns: usize,
82 data: Vec<u8>,
83 attributes: Vec<Option<u8>>,
84 pub cursor: usize,
85 pub alarm: bool,
86 pub keyboard_locked: bool,
87}
88
89impl Screen {
90 pub fn new(rows: usize, columns: usize) -> Self {
91 let size = rows * columns;
92 Self { rows, columns, data: vec![0; size], attributes: vec![None; size], cursor: 0, alarm: false, keyboard_locked: true }
93 }
94
95 pub fn size(&self) -> usize {
96 self.rows * self.columns
97 }
98
99 pub fn position(&self, address: usize) -> (usize, usize) {
101 (address / self.columns + 1, address % self.columns + 1)
102 }
103
104 pub fn address(&self, row: usize, column: usize) -> usize {
105 ((row - 1) * self.columns + column - 1) % self.size()
106 }
107
108 fn erase(&mut self) {
109 self.data.fill(0);
110 self.attributes.fill(None);
111 self.cursor = 0;
112 }
113
114 pub fn fields(&self) -> Vec<FieldView> {
116 let size = self.size();
117 let starts: Vec<usize> = (0..size).filter(|&a| self.attributes[a].is_some()).collect();
118 starts
119 .iter()
120 .enumerate()
121 .map(|(i, &at)| {
122 let next = starts[(i + 1) % starts.len()];
123 let len = (next + size - at - 1) % size;
124 FieldView { attribute_at: at, attribute: self.attributes[at].unwrap_or(0), start: (at + 1) % size, len: if starts.len() == 1 { size - 1 } else { len } }
125 })
126 .collect()
127 }
128
129 pub fn field_at(&self, address: usize) -> Option<FieldView> {
131 let size = self.size();
132 self.fields().into_iter().find(|f| f.attribute_at == address || (address + size - f.start) % size < f.len)
133 }
134
135 pub fn field_data(&self, field: &FieldView) -> Vec<u8> {
136 (0..field.len).map(|i| self.data[(field.start + i) % self.size()]).collect()
137 }
138
139 fn set_attribute(&mut self, address: usize, attribute: u8) {
140 self.attributes[address] = Some(attribute);
141 self.data[address] = 0;
142 }
143
144 fn put(&mut self, address: &mut usize, byte: u8) {
145 self.attributes[*address] = None;
146 self.data[*address] = byte;
147 *address = (*address + 1) % self.size();
148 }
149
150 fn next_unprotected(&self, from: usize) -> usize {
151 let size = self.size();
152 (1..=size)
153 .map(|i| (from + i) % size)
154 .find(|&a| self.attributes[a].is_some_and(|attr| attr & PROTECTED == 0))
155 .map_or(0, |a| (a + 1) % size)
156 }
157
158 pub fn apply(&mut self, stream: &[u8]) -> Result<(), String> {
160 let (&command, rest) = stream.split_first().ok_or("an empty data stream")?;
161 match command {
162 ERASE_WRITE | ERASE_WRITE_ALTERNATE | 0x05 | 0x0D => self.erase(),
163 WRITE | 0x01 => {}
164 ERASE_ALL_UNPROTECTED | 0x0F => {
165 let size = self.size();
166 for f in self.fields().iter().filter(|f| f.attribute & PROTECTED == 0) {
167 for i in 0..f.len {
168 self.data[(f.start + i) % size] = 0;
169 }
170 self.attributes[f.attribute_at] = Some(f.attribute & !MDT);
171 }
172 self.keyboard_locked = false;
173 return Ok(());
174 }
175 other => return Err(format!("3270 command X'{other:02X}' is not supported")),
176 }
177 let Some((&wcc, mut orders)) = rest.split_first() else { return Ok(()) };
178 if wcc & WCC_RESET_MDT != 0 {
179 for a in 0..self.size() {
180 if let Some(attr) = self.attributes[a] {
181 self.attributes[a] = Some(attr & !MDT);
182 }
183 }
184 }
185 let mut at = self.cursor;
186 let size = self.size();
187 let address = |bytes: &[u8]| -> Result<usize, String> {
188 let [a, b] = bytes.get(..2).and_then(|s| s.try_into().ok()).ok_or("a buffer address cut short")?;
189 let address = decode_address([a, b]);
190 if address >= size { Err(format!("buffer address {address} is past the screen ({size})")) } else { Ok(address) }
191 };
192 while let Some((&byte, rest)) = orders.split_first() {
193 orders = rest;
194 match byte {
195 SBA => {
196 at = address(orders)?;
197 orders = &orders[2..];
198 }
199 SF => {
200 let (&attr, rest) = orders.split_first().ok_or("SF without an attribute")?;
201 self.set_attribute(at, attr);
202 at = (at + 1) % size;
203 orders = rest;
204 }
205 SFE => {
206 let (&pairs, rest) = orders.split_first().ok_or("SFE without a count")?;
207 let pairs = usize::from(pairs);
208 let list = rest.get(..pairs * 2).ok_or("SFE cut short")?;
209 let attr = list.chunks(2).find(|p| p[0] == 0xC0).map_or(0x40, |p| p[1]);
210 self.set_attribute(at, attr);
211 at = (at + 1) % size;
212 orders = &rest[pairs * 2..];
213 }
214 SA => orders = orders.get(2..).ok_or("SA cut short")?,
215 MF => {
216 let (&pairs, rest) = orders.split_first().ok_or("MF without a count")?;
217 orders = rest.get(usize::from(pairs) * 2..).ok_or("MF cut short")?;
218 }
219 IC => self.cursor = at,
220 PT => at = self.next_unprotected(at),
221 RA => {
222 let stop = address(orders)?;
223 let fill = *orders.get(2).ok_or("RA without a character")?;
224 orders = &orders[3..];
225 loop {
226 self.put(&mut at, fill);
227 if at == stop {
228 break;
229 }
230 }
231 }
232 EUA => {
233 let stop = address(orders)?;
234 orders = &orders[2..];
235 while at != stop {
236 if self.field_at(at).is_some_and(|f| f.attribute & PROTECTED == 0) && self.attributes[at].is_none() {
237 self.data[at] = 0;
238 }
239 at = (at + 1) % size;
240 }
241 }
242 data => self.put(&mut at, data),
243 }
244 }
245 self.alarm = wcc & WCC_ALARM != 0;
246 if wcc & WCC_RESTORE != 0 {
247 self.keyboard_locked = false;
248 }
249 Ok(())
250 }
251
252 pub fn type_at(&mut self, address: usize, text: &[u8]) -> Result<(), String> {
255 let field = self.field_at(address).ok_or("the screen has no fields")?;
256 if field.attribute & PROTECTED != 0 || field.attribute_at == address {
257 return Err(format!("row {} column {} is protected", self.position(address).0, self.position(address).1));
258 }
259 let size = self.size();
260 let offset = (address + size - field.start) % size;
261 for (i, &b) in text.iter().take(field.len - offset).enumerate() {
262 self.data[(address + i) % size] = b;
263 }
264 self.attributes[field.attribute_at] = Some(field.attribute | MDT);
265 self.cursor = (address + text.len().min(field.len - offset)) % size;
266 Ok(())
267 }
268
269 pub fn erase_eof(&mut self, address: usize) -> Result<(), String> {
271 let field = self.field_at(address).ok_or("the screen has no fields")?;
272 if field.attribute & PROTECTED != 0 {
273 return Err("ERASE EOF in a protected field".into());
274 }
275 let size = self.size();
276 let offset = (address + size - field.start) % size;
277 for i in offset..field.len {
278 self.data[(field.start + i) % size] = 0;
279 }
280 self.attributes[field.attribute_at] = Some(field.attribute | MDT);
281 Ok(())
282 }
283
284 pub fn read_modified(&self, aid: u8) -> Vec<u8> {
287 if matches!(aid, AID_CLEAR | AID_PA1 | AID_PA2 | AID_PA3) {
288 return vec![aid];
289 }
290 let mut out = vec![aid];
291 out.extend(encode_address(self.cursor));
292 for f in self.fields().iter().filter(|f| f.attribute & MDT != 0) {
293 out.push(SBA);
294 out.extend(encode_address(f.start));
295 out.extend(self.field_data(f).into_iter().filter(|&b| b != 0));
296 }
297 out
298 }
299
300 pub fn render(&self, page: &CodePage) -> String {
302 let mut shown: Vec<char> = self.data.iter().map(|&b| if b == 0 { ' ' } else { page.decode_byte(b) }).collect();
303 for f in self.fields() {
304 shown[f.attribute_at] = ' ';
305 if f.attribute & DISPLAY == NON_DISPLAY {
306 for i in 0..f.len {
307 shown[(f.start + i) % self.size()] = ' ';
308 }
309 }
310 }
311 shown.chunks(self.columns).map(|row| row.iter().collect::<String>().trim_end().to_owned()).collect::<Vec<_>>().join("\n")
312 }
313}
314
315#[derive(Clone, Debug, PartialEq, Eq)]
318pub struct Inbound {
319 pub aid: u8,
320 pub cursor: Option<usize>,
321 pub fields: Vec<(usize, Vec<u8>)>,
322}
323
324pub fn parse_inbound(stream: &[u8]) -> Result<Inbound, String> {
325 let (&aid, rest) = stream.split_first().ok_or("an empty inbound stream")?;
326 if rest.is_empty() {
327 return Ok(Inbound { aid, cursor: None, fields: Vec::new() });
328 }
329 let cursor = decode_address(rest.get(..2).and_then(|s| s.try_into().ok()).ok_or("the cursor address is cut short")?);
330 let mut fields = Vec::new();
331 let mut i = 2;
332 while i < rest.len() {
333 if rest[i] != SBA {
334 return Err(format!("expected SBA at byte {}", i + 1));
335 }
336 let at = decode_address(rest.get(i + 1..i + 3).and_then(|s| s.try_into().ok()).ok_or("an SBA address is cut short")?);
337 let end = rest[i + 3..].iter().position(|&b| b == SBA).map_or(rest.len(), |p| i + 3 + p);
338 fields.push((at, rest[i + 3..end].to_vec()));
339 i = end;
340 }
341 Ok(Inbound { aid, cursor: Some(cursor), fields })
342}
343
344pub fn aid_of(key: &str) -> Option<u8> {
346 let key = key.to_ascii_uppercase();
347 let pf = |n: u8| -> u8 {
348 match n {
349 1..=9 => 0xF0 + n,
350 10..=12 => 0x7A + n - 10,
351 13..=21 => 0xC1 + n - 13,
352 _ => 0x4A + n - 22,
353 }
354 };
355 Some(match key.as_str() {
356 "ENTER" => AID_ENTER,
357 "CLEAR" => AID_CLEAR,
358 "PA1" => AID_PA1,
359 "PA2" => AID_PA2,
360 "PA3" => AID_PA3,
361 k => match k.strip_prefix("PF").and_then(|n| n.parse::<u8>().ok()) {
362 Some(n @ 1..=24) => pf(n),
363 _ => return None,
364 },
365 })
366}
367
368#[derive(Clone, Debug, PartialEq, Eq)]
370pub enum Action {
371 Type { row: usize, column: usize, text: String },
372 EraseEof { row: usize, column: usize },
373 Cursor { row: usize, column: usize },
374 Key(u8),
375}
376
377pub fn parse_script(text: &str) -> Result<Vec<Action>, String> {
380 let mut actions = Vec::new();
381 for (n, raw) in text.lines().enumerate() {
382 let line = raw.trim();
383 if line.is_empty() || line.starts_with('#') {
384 continue;
385 }
386 let bad = |why: &str| format!("screen script line {}: {why}: {raw}", n + 1);
387 let mut words = line.splitn(4, ' ');
388 let verb = words.next().unwrap_or("").to_ascii_lowercase();
389 let at = |words: &mut std::str::SplitN<'_, char>| -> Result<(usize, usize), String> {
390 let row = words.next().and_then(|w| w.parse().ok()).filter(|&r: &usize| r >= 1).ok_or_else(|| bad("expected a row from 1"))?;
391 let column = words.next().and_then(|w| w.parse().ok()).filter(|&c: &usize| c >= 1).ok_or_else(|| bad("expected a column from 1"))?;
392 Ok((row, column))
393 };
394 actions.push(match verb.as_str() {
395 "type" => {
396 let (row, column) = at(&mut words)?;
397 Action::Type { row, column, text: words.next().unwrap_or("").to_owned() }
398 }
399 "eof" => {
400 let (row, column) = at(&mut words)?;
401 Action::EraseEof { row, column }
402 }
403 "cursor" => {
404 let (row, column) = at(&mut words)?;
405 Action::Cursor { row, column }
406 }
407 key => Action::Key(aid_of(key).ok_or_else(|| bad("not type, eof, cursor or an AID key"))?),
408 });
409 }
410 Ok(actions)
411}
412
413pub struct Scripted {
416 pub screen: Screen,
417 actions: std::collections::VecDeque<Action>,
418 page: &'static CodePage,
419 pub shown: std::rc::Rc<std::cell::RefCell<Vec<String>>>,
420}
421
422impl std::fmt::Debug for Scripted {
423 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
424 write!(f, "Scripted({}x{}, {} actions left)", self.screen.rows, self.screen.columns, self.actions.len())
425 }
426}
427
428impl Scripted {
429 pub fn new(rows: usize, columns: usize, actions: Vec<Action>, page: &'static CodePage) -> Self {
430 Self { screen: Screen::new(rows, columns), actions: actions.into(), page, shown: Default::default() }
431 }
432}
433
434impl crate::cics::Terminal for Scripted {
435 fn size(&self) -> (usize, usize) {
436 (self.screen.rows, self.screen.columns)
437 }
438
439 fn send(&mut self, stream: &[u8]) -> Result<(), String> {
440 self.screen.apply(stream)?;
441 self.shown.borrow_mut().push(self.screen.render(self.page));
442 Ok(())
443 }
444
445 fn receive(&mut self) -> Result<Option<Vec<u8>>, String> {
446 while let Some(action) = self.actions.pop_front() {
447 match action {
448 Action::Type { row, column, text } => {
449 let bytes: Vec<u8> = text.chars().map(|c| self.page.encode_char(c).unwrap_or(0x6F)).collect();
450 self.screen.type_at(self.screen.address(row, column), &bytes)?;
451 }
452 Action::EraseEof { row, column } => self.screen.erase_eof(self.screen.address(row, column))?,
453 Action::Cursor { row, column } => self.screen.cursor = self.screen.address(row, column),
454 Action::Key(aid) => return Ok(Some(self.screen.read_modified(aid))),
455 }
456 }
457 Ok(None)
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 fn page() -> &'static CodePage {
466 CodePage::by_ccsid(37).unwrap()
467 }
468
469 #[test]
470 fn addresses_round_trip_in_both_encodings() {
471 for a in [0, 1, 79, 80, 1919, 4095] {
472 assert_eq!(decode_address(encode_address(a)), a);
473 }
474 assert_eq!(encode_address(80), [0xC1, 0x50]);
475 assert_eq!(decode_address(encode_address(5000)), 5000);
476 assert_eq!(encode_address(5000)[0] & 0xC0, 0);
477 }
478
479 #[test]
480 fn a_write_builds_fields_and_read_modified_returns_what_was_typed() {
481 let mut s = Screen::new(24, 80);
482 let mut stream = vec![ERASE_WRITE, WCC_RESTORE, SBA];
483 stream.extend(encode_address(0));
484 stream.extend([SF, attribute_byte(PROTECTED | INTENSIFIED)]);
485 stream.extend(page().encode("NAME:").unwrap());
486 stream.extend([SF, attribute_byte(0), IC]);
487 stream.extend([SBA]);
488 stream.extend(encode_address(20));
489 stream.extend([SF, attribute_byte(PROTECTED)]);
490 s.apply(&stream).unwrap();
491 assert_eq!(s.fields().len(), 3);
492 assert_eq!(s.cursor, 7);
493 assert!(!s.keyboard_locked);
494 assert!(s.type_at(1, b"x").is_err());
495 s.type_at(7, &page().encode("SMITH").unwrap()).unwrap();
496 assert_eq!(s.render(page()).lines().next().unwrap(), " NAME: SMITH");
497 let inbound = s.read_modified(AID_ENTER);
498 let read = parse_inbound(&inbound).unwrap();
499 assert_eq!((read.aid, read.cursor), (AID_ENTER, Some(12)));
500 assert_eq!(read.fields, vec![(7, page().encode("SMITH").unwrap())]);
501 assert_eq!(s.read_modified(AID_CLEAR), vec![AID_CLEAR]);
502 }
503
504 #[test]
505 fn wcc_resets_modified_bits_and_eau_clears_unprotected_data() {
506 let mut s = Screen::new(24, 80);
507 let mut stream = vec![ERASE_WRITE, WCC_RESTORE, SF, attribute_byte(0)];
508 stream.extend(page().encode("AB").unwrap());
509 s.apply(&stream).unwrap();
510 s.type_at(1, &page().encode("Z").unwrap()).unwrap();
511 assert_eq!(s.fields()[0].attribute & MDT, MDT);
512 s.apply(&[WRITE, WCC_RESET_MDT]).unwrap();
513 assert_eq!(s.fields()[0].attribute & MDT, 0);
514 s.apply(&[ERASE_ALL_UNPROTECTED]).unwrap();
515 assert!(s.field_data(&s.fields()[0]).iter().all(|&b| b == 0));
516 }
517
518 #[test]
519 fn non_display_fields_render_blank_and_bad_streams_are_refused() {
520 let mut s = Screen::new(2, 10);
521 let mut stream = vec![ERASE_WRITE, 0, SF, attribute_byte(NON_DISPLAY)];
522 stream.extend(page().encode("SECRET").unwrap());
523 s.apply(&stream).unwrap();
524 assert_eq!(s.render(page()), "\n");
525 assert!(s.apply(&[0x99]).is_err());
526 assert!(s.apply(&[WRITE, 0, SBA, 0x7F]).is_err());
527 }
528
529 #[test]
530 fn aid_keys_and_screen_scripts() {
531 assert_eq!(aid_of("enter"), Some(0x7D));
532 assert_eq!(aid_of("PF1"), Some(0xF1));
533 assert_eq!(aid_of("PF12"), Some(0x7C));
534 assert_eq!(aid_of("PF13"), Some(0xC1));
535 assert_eq!(aid_of("PF24"), Some(0x4C));
536 assert_eq!(aid_of("PF25"), None);
537 let script = parse_script("# order entry\ntype 5 20 SMITH & CO\neof 6 20\ncursor 7 1\nENTER\nPF3\n").unwrap();
538 assert_eq!(script[0], Action::Type { row: 5, column: 20, text: "SMITH & CO".into() });
539 assert_eq!(script[1], Action::EraseEof { row: 6, column: 20 });
540 assert_eq!(script[3], Action::Key(0x7D));
541 assert_eq!(script[4], Action::Key(0xF3));
542 assert!(parse_script("type 0 1 X").is_err());
543 assert!(parse_script("jump 1 1").is_err());
544 }
545}