This is a UPnP client library for Rust.

Fix #1: parser crashes for devices without serviceList

When the xml is missing the serviceList entirely, the parse_services() method
now returns an empty list.
Also added a unit-test with the crashing xml.

Changed files
+81 -43
src
+81 -43
src/parser.rs
··· 96 96 let device = root 97 97 .find("{urn:schemas-upnp-org:device-1-0}device") 98 98 .unwrap(); 99 - let service_list = device.find("{urn:schemas-upnp-org:device-1-0}serviceList"); 100 - let services = service_list.unwrap().children(); 101 99 102 - let services: Vec<Service> = services 103 - .into_iter() 104 - .map(|item| Service { 105 - service_type: item 106 - .find("{urn:schemas-upnp-org:device-1-0}serviceType") 107 - .unwrap() 108 - .text() 109 - .to_string(), 110 - service_id: item 111 - .find("{urn:schemas-upnp-org:device-1-0}serviceId") 112 - .unwrap() 113 - .text() 114 - .to_string(), 115 - control_url: item 116 - .find("{urn:schemas-upnp-org:device-1-0}controlURL") 117 - .unwrap() 118 - .text() 119 - .to_string(), 120 - event_sub_url: item 121 - .find("{urn:schemas-upnp-org:device-1-0}eventSubURL") 122 - .unwrap() 123 - .text() 124 - .to_string(), 125 - scpd_url: item 126 - .find("{urn:schemas-upnp-org:device-1-0}SCPDURL") 127 - .unwrap() 128 - .text() 129 - .to_string(), 130 - actions: vec![], 131 - }) 132 - .map(|mut service| { 133 - service.control_url = build_absolute_url(base_url, &service.control_url); 134 - service.event_sub_url = build_absolute_url(base_url, &service.event_sub_url); 135 - service.scpd_url = build_absolute_url(base_url, &service.scpd_url); 136 - service 137 - }) 138 - .collect(); 139 100 let mut services_with_actions: Vec<Service> = vec![]; 140 - for service in &services { 141 - let mut service = service.clone(); 142 - service.actions = parse_service_description(&service.scpd_url).await; 143 - services_with_actions.push(service); 101 + if let Some(service_list) = device.find("{urn:schemas-upnp-org:device-1-0}serviceList") { 102 + let services = service_list.children(); 103 + 104 + let services: Vec<Service> = services 105 + .into_iter() 106 + .map(|item| Service { 107 + service_type: item 108 + .find("{urn:schemas-upnp-org:device-1-0}serviceType") 109 + .unwrap() 110 + .text() 111 + .to_string(), 112 + service_id: item 113 + .find("{urn:schemas-upnp-org:device-1-0}serviceId") 114 + .unwrap() 115 + .text() 116 + .to_string(), 117 + control_url: item 118 + .find("{urn:schemas-upnp-org:device-1-0}controlURL") 119 + .unwrap() 120 + .text() 121 + .to_string(), 122 + event_sub_url: item 123 + .find("{urn:schemas-upnp-org:device-1-0}eventSubURL") 124 + .unwrap() 125 + .text() 126 + .to_string(), 127 + scpd_url: item 128 + .find("{urn:schemas-upnp-org:device-1-0}SCPDURL") 129 + .unwrap() 130 + .text() 131 + .to_string(), 132 + actions: vec![], 133 + }) 134 + .map(|mut service| { 135 + service.control_url = build_absolute_url(base_url, &service.control_url); 136 + service.event_sub_url = build_absolute_url(base_url, &service.event_sub_url); 137 + service.scpd_url = build_absolute_url(base_url, &service.scpd_url); 138 + service 139 + }) 140 + .collect(); 141 + 142 + for service in &services { 143 + let mut service = service.clone(); 144 + service.actions = parse_service_description(&service.scpd_url).await; 145 + services_with_actions.push(service); 146 + } 144 147 } 148 + 145 149 services_with_actions 146 150 } 147 151 ··· 755 759 } 756 760 Ok(transport_info) 757 761 } 762 + 763 + #[cfg(test)] 764 + mod tests { 765 + use crate::parser::parse_services; 766 + 767 + #[tokio::test] 768 + async fn test_parsing_device_without_service_list() { 769 + const XML_ROOT: &'static str = r#"<?xml version="1.0" encoding="UTF-8"?> 770 + <root xmlns="urn:schemas-upnp-org:device-1-0"> 771 + <specVersion> 772 + <major>1</major> 773 + <minor>0</minor> 774 + </specVersion> 775 + <device> 776 + <deviceType>urn:schemas-upnp-org:device:WLANAccessPointDevice:1</deviceType> 777 + <friendlyName>NETGEAR47B64C</friendlyName> 778 + <manufacturer>NETGEAR</manufacturer> 779 + <manufacturerURL>https://www.netgear.com</manufacturerURL> 780 + <modelDescription>NETGEAR Dual Band Access Point</modelDescription> 781 + <modelName>WAX214</modelName> 782 + <modelNumber>WAX214</modelNumber> 783 + <modelURL>https://www.netgear.com</modelURL> 784 + <firmwareVersion>2.1.1.3</firmwareVersion> 785 + <insightMode>0</insightMode> 786 + <serialNumber>XXXXXXXXX</serialNumber> 787 + <UDN>uuid:919ba4ec-ec93-490f-b0e3-80CC9C47B64C</UDN> 788 + <presentationURL>http://xxxxxx:1337/</presentationURL> 789 + </device> 790 + </root>"#; 791 + 792 + let result = parse_services("http://xxxxxx:1337/", XML_ROOT).await; 793 + assert_eq!(result.len(), 0); 794 + } 795 + }