1use std::collections::{BTreeMap, HashSet};
13use std::path::Path;
14
15use crate::{Error, Result};
16
17const FMT: &str = "PowerWorld .pwd";
18
19const IDENTITY_TAG: [u8; 6] = [0xff, 0xff, 0xff, 0xff, 0x3d, 0x0f];
21
22const HEADER_TRAILER: u32 = 10105;
26
27const UNTITLED_STAMP_AT: usize = 22;
29
30const MAX_TITLE_LEN: usize = 256;
34
35const IDENTITY_WALK_BUDGET: u64 = 128_000_000;
40
41const IDENTITY_ROW_BUDGET: usize = 1_000_000;
52const IDENTITY_NAME_BYTE_BUDGET: usize = 64 << 20;
53
54#[derive(Debug, Clone, PartialEq)]
58pub struct PwdSubstation {
59 pub number: u32,
60 pub name: String,
61 pub x: f64,
62 pub y: f64,
63}
64
65#[derive(Debug, Clone, PartialEq)]
71pub struct PwdDisplay {
72 pub canvas_width: u16,
73 pub canvas_height: u16,
74 pub stamp: u32,
75 pub substations: Vec<PwdSubstation>,
76}
77
78pub fn parse_pwd_file(path: impl AsRef<Path>) -> Result<PwdDisplay> {
84 let bytes = std::fs::read(path)?;
85 parse_pwd_display(&bytes)
86}
87
88pub fn parse_pwd_display(bytes: &[u8]) -> Result<PwdDisplay> {
94 parse_pwd_inner(bytes)
95}
96
97pub fn parse_pwd(bytes: &[u8]) -> Result<Vec<PwdSubstation>> {
103 parse_pwd_display(bytes).map(|display| display.substations)
104}
105
106pub fn parse_pwd_layer(bytes: &[u8]) -> Result<crate::geo::GeoParsed> {
109 use crate::geo::{ElementKey, GeoFeature, GeoGeometry, GeoTarget};
110 let display = parse_pwd_display(bytes)?;
111 let mut layer = crate::geo::to_geo_layer_from_pwd(&display);
112 let mut diagnostics = Vec::new();
113 let identities = bus_identities(bytes)?;
114 if !identities.is_empty() {
115 let mut buses = BTreeMap::new();
116 let mut owners = Vec::new();
117 for at in 0..bytes.len().saturating_sub(38) {
118 let Some((x, y)) = drawing_position(bytes, at, display.stamp) else {
119 continue;
120 };
121 match u16_at(bytes, at) {
122 Some(0x277e) => {
123 let Some(end) = style_label_end(bytes, at, &[67]) else {
124 continue;
125 };
126 let marker = end + 17;
127 if bytes.get(marker) != Some(&3) {
128 continue;
129 }
130 let Some(number) =
131 u32_at(bytes, marker + 1).filter(|n| identities.contains_key(n))
132 else {
133 continue;
134 };
135 if buses.insert(number, [x, y]).is_some() {
136 return Err(pwd_err("duplicate bus drawing identity"));
137 }
138 }
139 Some(0x27b3 | 0x27b7) => owners.push(at),
140 _ => {}
141 }
142 }
143 for (&number, &point) in &buses {
144 layer.features.push(GeoFeature {
145 target: GeoTarget::Bus,
146 key: ElementKey {
147 id: Some(number.to_string()),
148 name: Some(identities[&number].clone()),
149 ..ElementKey::default()
150 },
151 geometry: GeoGeometry::Point(point),
152 from: None,
153 to: None,
154 kind: None,
155 });
156 }
157 let mut unsupported = identities.len().saturating_sub(buses.len());
158 for at in owners {
159 let decoded = decode_branch_drawing(bytes, at, display.stamp, &buses);
160 if let Some(feature) = decoded {
161 layer.features.push(feature);
162 } else {
163 unsupported += 1;
164 }
165 }
166 if unsupported > 0 {
167 diagnostics.push(crate::diagnostics::Diagnostic::of(&crate::diagnostics::codes::READ_GEO_SOURCE_MALFORMED,
168 format!("{unsupported} bus or branch drawing objects have unsupported layouts or unmatched identities")));
169 }
170 }
171 if layer.features.is_empty() {
172 return Err(pwd_err(
173 "no supported bus, branch, or substation positions were decoded from the drawing",
174 ));
175 }
176 Ok(crate::geo::GeoParsed { layer, diagnostics })
177}
178
179fn decode_branch_drawing(
180 bytes: &[u8],
181 at: usize,
182 stamp: u32,
183 buses: &BTreeMap<u32, [f64; 2]>,
184) -> Option<crate::geo::GeoFeature> {
185 use crate::geo::{ElementKey, GeoFeature, GeoGeometry, GeoTarget};
186 let end = style_label_end(bytes, at, &[75, 79])?;
187 let from = u32_at(bytes, end + 63)?;
188 let to = u32_at(bytes, end + 67)?;
189 if !buses.contains_key(&from) || !buses.contains_key(&to) {
190 return None;
191 }
192 let child = end + 72;
193 if u16_at(bytes, child) != Some(0x3131) || drawing_position(bytes, child, stamp).is_none() {
194 return None;
195 }
196 let child_end = style_label_end(bytes, child, &[75, 79])?;
197 let count_at = child_end + 58;
198 let count = u32_at(bytes, count_at)? as usize;
199 if !(2..=100_000).contains(&count) {
200 return None;
201 }
202 let raw = bytes.get(count_at + 4..count_at.checked_add(4 + count * 16)?)?;
203 let mut points = Vec::with_capacity(count);
204 for i in (0..raw.len()).step_by(16) {
205 let x = f64_at(raw, i)?;
206 let y = f64_at(raw, i + 8)?;
207 if !x.is_finite() || !y.is_finite() || x.abs().max(y.abs()) > 1e7 {
208 return None;
209 }
210 points.push([x, y]);
211 }
212 Some(GeoFeature {
213 target: GeoTarget::Branch,
214 key: ElementKey::default(),
215 geometry: GeoGeometry::LineString(points),
216 from: Some(from.to_string()),
217 to: Some(to.to_string()),
218 kind: None,
219 })
220}
221
222fn drawing_position(bytes: &[u8], at: usize, stamp: u32) -> Option<(f64, f64)> {
223 if u32_at(bytes, at + 18) != Some(stamp) {
224 return None;
225 }
226 let x = f64_at(bytes, at + 22)?;
227 let y = f64_at(bytes, at + 30)?;
228 if !x.is_finite() || !y.is_finite() || x.abs().max(y.abs()) > 1e7 {
229 return None;
230 }
231 #[allow(clippy::cast_possible_truncation)]
232 let (rx, ry) = (x as f32, y as f32);
233 if f32_at(bytes, at + 2)?.to_bits() != rx.to_bits()
234 || f32_at(bytes, at + 6)?.to_bits() != ry.to_bits()
235 {
236 return None;
237 }
238 Some((x, y))
239}
240
241fn style_label_end(bytes: &[u8], at: usize, offsets: &[usize]) -> Option<usize> {
242 let mut found = None;
243 for offset in offsets {
244 let length = u32_at(bytes, at + offset)? as usize;
245 if length == 0 || length > 64 {
246 continue;
247 }
248 let end = (at + offset + 4).checked_add(length)?;
249 let text = bytes.get(at + offset + 4..end)?;
250 if !text.iter().all(|c| (0x20..0x7f).contains(c)) || u32_at(bytes, end) != Some(u32::MAX) {
251 continue;
252 }
253 if found.replace(end).is_some() {
254 return None;
255 }
256 }
257 found
258}
259
260fn bus_identities(bytes: &[u8]) -> Result<BTreeMap<u32, String>> {
261 let mut found = None;
262 let mut work = 0usize;
263 for anchor in memmem(bytes, &[0x3c, 0x0f]) {
264 let mut at = anchor + 2;
265 let mut rows = BTreeMap::new();
266 while work < IDENTITY_ROW_BUDGET {
267 work += 1;
268 if bytes.get(at..at + 6) == Some(&IDENTITY_TAG) {
269 if !rows.is_empty() && found.replace(rows).is_some() {
270 return Err(pwd_err("ambiguous bus identity tables"));
271 }
272 break;
273 }
274 let row = (|| -> Option<(u32, String, usize)> {
275 let number = u32_at(bytes, at)?;
276 let length = u32_at(bytes, at + 4)? as usize;
277 if number == 0 || number > 99_999_999 || length == 0 || length > 64 {
278 return None;
279 }
280 let end = at + 8 + length;
281 let name = bytes.get(at + 8..end)?;
282 if !name.iter().all(|c| (0x20..0x7f).contains(c))
283 || u32_at(bytes, end) != Some(number)
284 || bytes.get(end + 4) != Some(&0)
285 {
286 return None;
287 }
288 let label_length = u32_at(bytes, end + 5)? as usize;
289 if label_length > 64 {
290 return None;
291 }
292 let kv = f32_at(bytes, end + 9 + label_length)?;
293 if !kv.is_finite() || !(0.0..=10_000.0).contains(&kv) {
294 return None;
295 }
296 Some((
297 number,
298 String::from_utf8_lossy(name).into_owned(),
299 end + 13 + label_length,
300 ))
301 })();
302 let Some((number, name, next)) = row else {
303 break;
304 };
305 if rows.insert(number, name).is_some() {
306 break;
307 }
308 at = next;
309 }
310 if work >= IDENTITY_ROW_BUDGET {
311 return Err(pwd_err("bus identity search exceeded its work limit"));
312 }
313 }
314 Ok(found.unwrap_or_default())
315}
316
317fn pwd_err(message: impl Into<String>) -> Error {
318 Error::FormatRead {
319 format: FMT,
320 message: message.into(),
321 }
322}
323
324fn parse_pwd_header(bytes: &[u8]) -> Result<(u16, u16, u32)> {
325 let (Some(header), Some(canvas_width), Some(canvas_height)) =
326 (u32_at(bytes, 0), u16_at(bytes, 4), u16_at(bytes, 6))
327 else {
328 let header = u32_at(bytes, 0).unwrap_or(0);
329 return Err(pwd_err(format!(
330 "not a recognized PowerWorld display file (header word {header}; the probed saves all \
331 carry 50)",
332 )));
333 };
334 if bytes.len() < 0x40 || header != 50 {
335 return Err(pwd_err(format!(
336 "not a recognized PowerWorld display file (header word {header}; the probed saves all \
337 carry 50)",
338 )));
339 }
340 if canvas_width == 0 || canvas_height == 0 {
341 return Err(pwd_err("display header canvas dimensions are zero"));
342 }
343 let stamp = header_stamp(bytes).unwrap_or(0);
344 if stamp == 0 {
345 return Err(pwd_err(
346 "display header stamp is zero; every validated save carries a nonzero stamp the \
347 drawing records repeat",
348 ));
349 }
350 Ok((canvas_width, canvas_height, stamp))
351}
352
353fn header_stamp(bytes: &[u8]) -> Option<u32> {
363 let title_len = usize::from(u16_at(bytes, 10)?);
364 let titled_at = UNTITLED_STAMP_AT.checked_add(title_len)?;
365 let title_is_shaped = title_len <= MAX_TITLE_LEN
366 && u16_at(bytes, 12) == Some(0)
367 && bytes
368 .get(14..14 + title_len)
369 .is_some_and(|title| title.iter().all(|&c| (0x20..0x7f).contains(&c)))
370 && bytes
371 .get(14 + title_len..UNTITLED_STAMP_AT + title_len)
372 .is_some_and(|gap| gap.iter().all(|&c| c == 0));
373 if title_is_shaped
374 && u32_at(bytes, titled_at).is_some_and(|stamp| stamp != 0)
375 && u32_at(bytes, titled_at + 4) == Some(HEADER_TRAILER)
376 {
377 return u32_at(bytes, titled_at);
378 }
379 u32_at(bytes, UNTITLED_STAMP_AT)
380}
381
382fn parse_pwd_inner(bytes: &[u8]) -> Result<PwdDisplay> {
383 let (canvas_width, canvas_height, stamp) = parse_pwd_header(bytes)?;
384
385 let identity = find_identity_table(bytes)?;
386 if identity.is_empty() {
387 return Ok(PwdDisplay {
388 canvas_width,
389 canvas_height,
390 stamp,
391 substations: Vec::new(),
392 });
393 }
394
395 let mut groups: BTreeMap<u16, Vec<DrawRecord>> = BTreeMap::new();
402 for i in 0..bytes.len().saturating_sub(38) {
403 if u32_at(bytes, i + 18) != Some(stamp) {
404 continue;
405 }
406 let (Some(x), Some(y)) = (f64_at(bytes, i + 22), f64_at(bytes, i + 30)) else {
407 continue;
408 };
409 if !x.is_finite() || !y.is_finite() {
410 continue;
411 }
412 #[allow(clippy::cast_possible_truncation)] let (rx, ry) = (x as f32, y as f32);
414 if f32_at(bytes, i + 2).map(f32::to_bits) != Some(rx.to_bits())
417 || f32_at(bytes, i + 6).map(f32::to_bits) != Some(ry.to_bits())
418 {
419 continue;
420 }
421 let magnitude = x.abs().max(y.abs());
422 if !(1.0..1.0e7).contains(&magnitude) {
423 continue;
424 }
425 let Some(tag) = u16_at(bytes, i) else {
426 continue;
427 };
428 let rec = DrawRecord { at: i, x, y };
429 groups.entry(tag).or_default().push(rec);
430 }
431
432 let matches: Vec<(&u16, &Vec<DrawRecord>)> = groups
438 .iter()
439 .filter(|(_, records)| {
440 records.len() == identity.len()
441 && records
442 .iter()
443 .zip(&identity)
444 .all(|(rec, (number, _))| links_number(bytes, rec.at, *number))
445 })
446 .collect();
447 let (_, records) = match matches.as_slice() {
448 [one] => *one,
449 [] => {
450 return Err(pwd_err(format!(
451 "no drawing record group links the {} substation identity rows; the \
452 DisplaySubstation layout of this save is not the validated one",
453 identity.len()
454 )));
455 }
456 several => {
457 return Err(pwd_err(format!(
458 "{} drawing record groups link the substation identity rows; refusing to guess \
459 between them",
460 several.len()
461 )));
462 }
463 };
464
465 let substations = records
466 .iter()
467 .zip(identity)
468 .map(|(rec, (number, name))| PwdSubstation {
469 number,
470 name,
471 x: rec.x,
472 y: rec.y,
473 })
474 .collect();
475 Ok(PwdDisplay {
476 canvas_width,
477 canvas_height,
478 stamp,
479 substations,
480 })
481}
482
483struct DrawRecord {
487 at: usize,
488 x: f64,
489 y: f64,
490}
491
492fn find_identity_table(b: &[u8]) -> Result<Vec<(u32, String)>> {
496 let mut budget = 0u64;
501 let mut retained = Retention::default();
502 let mut tables = Vec::new();
503 for at in memmem(b, &IDENTITY_TAG) {
504 if let Some(rows) = identity_walk(b, at + IDENTITY_TAG.len(), &mut budget, &mut retained) {
505 tables.push(rows);
506 }
507 if budget > IDENTITY_WALK_BUDGET {
508 return Err(Error::FormatRead {
509 format: FMT,
510 message: "substation identity search exceeded its probe budget; the file is \
511 not a decodable DisplaySubstation layout"
512 .into(),
513 });
514 }
515 if retained.exceeded {
516 return Err(Error::FormatRead {
517 format: FMT,
518 message: "substation identity search exceeded its retention budget; the file \
519 packs more identity rows than any decodable DisplaySubstation \
520 layout states"
521 .into(),
522 });
523 }
524 }
525 match tables.len() {
526 1 => Ok(tables.pop().unwrap()),
527 0 => Ok(Vec::new()),
528 n => Err(Error::FormatRead {
529 format: FMT,
530 message: format!(
531 "{n} byte ranges walk as a substation identity table; refusing to guess \
532 between them"
533 ),
534 }),
535 }
536}
537
538#[derive(Default)]
546struct Retention {
547 rows: usize,
548 name_bytes: usize,
549 exceeded: bool,
550}
551
552fn identity_walk(
553 b: &[u8],
554 mut at: usize,
555 budget: &mut u64,
556 retained: &mut Retention,
557) -> Option<Vec<(u32, String)>> {
558 let mut rows = Vec::new();
559 let mut seen = HashSet::new();
560 loop {
561 *budget = budget.saturating_add(1);
564 if *budget > IDENTITY_WALK_BUDGET {
565 return None;
566 }
567 if b.get(at..).and_then(|s| s.get(..4)) == Some([0xff; 4].as_slice()) {
568 return (!rows.is_empty()).then_some(rows);
569 }
570 let number = u32_at(b, at)?;
571 let duplicate_at = at.checked_add(4)?;
572 if number == 0 || number > 99_999_999 || u32_at(b, duplicate_at) != Some(number) {
573 return None;
574 }
575 let len_at = at.checked_add(8)?;
576 let len = u32_at(b, len_at)? as usize;
577 if len == 0 || len >= 64 {
578 return None;
579 }
580 let name_start = at.checked_add(12)?;
581 let name_end = name_start.checked_add(len)?;
582 let name = b.get(name_start..name_end)?;
583 if !name.iter().all(|&c| (0x20..0x7f).contains(&c)) || b.get(name_end) != Some(&0x02) {
584 return None;
585 }
586 if !seen.insert(number) {
587 return None;
588 }
589 retained.rows += 1;
590 retained.name_bytes += name.len();
591 if retained.rows > IDENTITY_ROW_BUDGET || retained.name_bytes > IDENTITY_NAME_BYTE_BUDGET {
592 retained.exceeded = true;
593 return None;
594 }
595 rows.push((number, String::from_utf8_lossy(name).into_owned()));
596 at = name_end.checked_add(1)?;
597 }
598}
599
600fn links_number(b: &[u8], i: usize, number: u32) -> bool {
606 (40..140).any(|d| {
607 let Some(marker_at) = i.checked_add(d) else {
608 return false;
609 };
610 let Some(number_at) = marker_at.checked_add(1) else {
611 return false;
612 };
613 matches!(b.get(marker_at), Some(0x03 | 0x07)) && u32_at(b, number_at) == Some(number)
614 })
615}
616
617fn memmem<'a>(haystack: &'a [u8], needle: &'a [u8]) -> impl Iterator<Item = usize> + 'a {
619 haystack
620 .windows(needle.len())
621 .enumerate()
622 .filter_map(move |(i, w)| (w == needle).then_some(i))
623}
624
625fn u16_at(b: &[u8], i: usize) -> Option<u16> {
630 Some(u16::from_le_bytes(*b.get(i..)?.first_chunk()?))
631}
632
633fn u32_at(b: &[u8], i: usize) -> Option<u32> {
634 Some(u32::from_le_bytes(*b.get(i..)?.first_chunk()?))
635}
636
637fn f32_at(b: &[u8], i: usize) -> Option<f32> {
638 Some(f32::from_le_bytes(*b.get(i..)?.first_chunk()?))
639}
640
641fn f64_at(b: &[u8], i: usize) -> Option<f64> {
642 Some(f64::from_le_bytes(*b.get(i..)?.first_chunk()?))
643}
644
645#[cfg(test)]
646mod retention_tests {
647 use super::*;
648
649 #[test]
653 fn packed_identity_rows_hit_the_retention_budget() {
654 let mut b = vec![0u8; 0x40];
657 b[0] = 50;
658 b[4] = 1; b[6] = 1; b[22] = 7; b.extend_from_slice(&IDENTITY_TAG);
662 let name = b"S";
663 for number in 1..=(IDENTITY_ROW_BUDGET as u32 + 2) {
664 b.extend_from_slice(&number.to_le_bytes());
665 b.extend_from_slice(&number.to_le_bytes());
666 b.extend_from_slice(&(name.len() as u32).to_le_bytes());
667 b.extend_from_slice(name);
668 b.push(0x02);
669 }
670 b.extend_from_slice(&[0xff; 4]);
671 let error = parse_pwd(&b).unwrap_err().to_string();
672 assert!(error.contains("retention budget"), "{error}");
673 }
674}
675
676#[cfg(test)]
677mod drawing_tests {
678 use super::*;
679
680 fn symbol(tag: u16, stamp: u32, x: f64, y: f64, size: usize) -> Vec<u8> {
681 let mut b = vec![0; size];
682 b[..2].copy_from_slice(&tag.to_le_bytes());
683 b[2..6].copy_from_slice(&(x as f32).to_le_bytes());
684 b[6..10].copy_from_slice(&(y as f32).to_le_bytes());
685 b[18..22].copy_from_slice(&stamp.to_le_bytes());
686 b[22..30].copy_from_slice(&x.to_le_bytes());
687 b[30..38].copy_from_slice(&y.to_le_bytes());
688 b
689 }
690
691 fn drawing() -> Vec<u8> {
692 let mut b = vec![0; 64];
693 b[..4].copy_from_slice(&50u32.to_le_bytes());
694 b[4..6].copy_from_slice(&200u16.to_le_bytes());
695 b[6..8].copy_from_slice(&200u16.to_le_bytes());
696 b[22..26].copy_from_slice(&7u32.to_le_bytes());
697 b[26..30].copy_from_slice(&10105u32.to_le_bytes());
698 b.extend_from_slice(&[0x3c, 0x0f]);
699 for number in [1u32, 2] {
700 b.extend_from_slice(&number.to_le_bytes());
701 b.extend_from_slice(&1u32.to_le_bytes());
702 b.push(b'A');
703 b.extend_from_slice(&number.to_le_bytes());
704 b.push(0);
705 b.extend_from_slice(&0u32.to_le_bytes());
706 b.extend_from_slice(&345f32.to_le_bytes());
707 }
708 b.extend_from_slice(&IDENTITY_TAG);
709 b.extend_from_slice(&[0xff, 0xff, 0xff, 0xff, 0x3e, 0x0f]);
710 for (number, x, y) in [(1u32, 28500., 18900.), (2u32, 29500., 17900.)] {
711 let mut row = symbol(0x277e, 7, x, y, 96);
712 row[67..71].copy_from_slice(&1u32.to_le_bytes());
713 row[71] = b'A';
714 row[72..76].fill(0xff);
715 row[89] = 3;
716 row[90..94].copy_from_slice(&number.to_le_bytes());
717 b.extend(row);
718 }
719 let mut line = symbol(0x27b3, 7, 29000., 18400., 156);
720 line[79..83].copy_from_slice(&1u32.to_le_bytes());
721 line[83] = b'A';
722 line[84..88].fill(0xff);
723 line[147..151].copy_from_slice(&1u32.to_le_bytes());
724 line[151..155].copy_from_slice(&2u32.to_le_bytes());
725 b.extend(line);
726 let mut route = symbol(0x3131, 7, 29000., 18400., 178);
727 route[79..83].copy_from_slice(&1u32.to_le_bytes());
728 route[83] = b'A';
729 route[84..88].fill(0xff);
730 route[142..146].copy_from_slice(&2u32.to_le_bytes());
731 for (i, value) in [28500f64, 18900., 29500., 17900.].iter().enumerate() {
732 route[146 + i * 8..154 + i * 8].copy_from_slice(&value.to_le_bytes());
733 }
734 b.extend(route);
735 b
736 }
737
738 #[test]
739 fn bus_and_route_positions_keep_diagram_units() {
740 let bytes = drawing();
741 let parsed = parse_pwd_layer(&bytes).unwrap();
742 assert!(parsed.diagnostics.is_empty());
743 assert_eq!(parsed.layer.features.len(), 3);
744 assert!(matches!(
745 parsed.layer.space,
746 crate::geo::CoordinateSpace::Diagram { .. }
747 ));
748 assert_eq!(
749 parsed.layer.features[0].geometry,
750 crate::geo::GeoGeometry::Point([28500., 18900.])
751 );
752 let route = &parsed.layer.features[2];
753 assert_eq!(route.from.as_deref(), Some("1"));
754 assert_eq!(route.to.as_deref(), Some("2"));
755 let encoded = parsed.layer.to_geojson_checked().unwrap();
756 assert_eq!(
757 crate::geo::GeoLayer::parse(&encoded, None).unwrap().layer,
758 parsed.layer
759 );
760 let truncated = parse_pwd_layer(&bytes[..bytes.len() - 8]).unwrap();
761 assert!(!truncated.diagnostics.is_empty());
762 assert_eq!(truncated.layer.features.len(), 2);
763 }
764
765 #[test]
766 fn empty_and_mismatched_drawing_records_fail_explicitly() {
767 let mut b = drawing();
768 assert!(parse_pwd_layer(&b[..64]).is_err());
769 let starts: Vec<_> = memmem(&b, &[0x7e, 0x27]).collect();
770 for at in starts {
771 b[at + 2..at + 6].fill(0);
772 }
773 assert!(parse_pwd_layer(&b).is_err());
774 }
775}