• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (C) 2023 Huawei Device Co., Ltd.
2 // Licensed under the Apache License, Version 2.0 (the "License");
3 // you may not use this file except in compliance with the License.
4 // You may obtain a copy of the License at
5 //
6 //     http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13 
14 use std::collections::{HashMap, HashSet};
15 use std::fs::File;
16 use std::os::fd::{FromRawFd, IntoRawFd, RawFd};
17 
18 pub use ffi::{Action, Mode};
19 use ipc::IpcStatusCode;
20 
21 cfg_oh! {
22     use ipc::parcel::Serialize;
23     use ipc::parcel::Deserialize;
24 }
25 
26 use super::reason::Reason;
27 use super::ATOMIC_SERVICE;
28 use crate::manage::account::GetOhosAccountUid;
29 use crate::manage::network::{NetworkState, NetworkType};
30 use crate::utils::c_wrapper::{CFileSpec, CFormItem, CStringWrapper};
31 use crate::utils::form_item::{FileSpec, FormItem};
32 use crate::utils::{hashmap_to_string, query_calling_bundle};
33 
34 #[cxx::bridge(namespace = "OHOS::Request")]
35 mod ffi {
36     /// Action
37     #[derive(Clone, Copy, PartialEq, Debug)]
38     #[repr(u8)]
39     pub enum Action {
40         /// Download
41         Download = 0,
42         /// Upload
43         Upload,
44         /// Any
45         Any,
46     }
47 
48     /// Mode
49     #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
50     #[repr(u8)]
51     pub enum Mode {
52         /// BackGround
53         BackGround = 0,
54         /// ForeGround
55         FrontEnd,
56         /// Any
57         Any,
58     }
59 }
60 
61 #[derive(Clone, Copy, PartialEq, Debug)]
62 #[repr(u8)]
63 pub(crate) enum Version {
64     API9 = 1,
65     API10,
66 }
67 
68 /// NetworkConfig
69 #[derive(Clone, Copy, PartialEq, Debug)]
70 #[repr(u8)]
71 pub enum NetworkConfig {
72     /// Any
73     Any = 0,
74     /// Wifi
75     Wifi,
76     /// Cellular
77     Cellular,
78 }
79 
80 #[repr(C)]
81 #[derive(Copy, Clone, Debug)]
82 pub(crate) struct CommonTaskConfig {
83     pub(crate) task_id: u32,
84     pub(crate) uid: u64,
85     pub(crate) token_id: u64,
86     pub(crate) action: Action,
87     pub(crate) mode: Mode,
88     pub(crate) cover: bool,
89     pub(crate) network_config: NetworkConfig,
90     pub(crate) metered: bool,
91     pub(crate) roaming: bool,
92     pub(crate) retry: bool,
93     pub(crate) redirect: bool,
94     pub(crate) index: u32,
95     pub(crate) begins: u64,
96     pub(crate) ends: i64,
97     pub(crate) gauge: bool,
98     pub(crate) precise: bool,
99     pub(crate) priority: u32,
100     pub(crate) background: bool,
101     pub(crate) multipart: bool,
102 }
103 
104 /// task config
105 #[derive(Clone, Debug)]
106 pub struct TaskConfig {
107     pub(crate) bundle: String,
108     pub(crate) bundle_type: u32,
109     pub(crate) atomic_account: String,
110     pub(crate) url: String,
111     pub(crate) title: String,
112     pub(crate) description: String,
113     pub(crate) method: String,
114     pub(crate) headers: HashMap<String, String>,
115     pub(crate) data: String,
116     pub(crate) token: String,
117     pub(crate) proxy: String,
118     pub(crate) certificate_pins: String,
119     pub(crate) extras: HashMap<String, String>,
120     pub(crate) version: Version,
121     pub(crate) form_items: Vec<FormItem>,
122     pub(crate) file_specs: Vec<FileSpec>,
123     pub(crate) body_file_paths: Vec<String>,
124     pub(crate) certs_path: Vec<String>,
125     pub(crate) common_data: CommonTaskConfig,
126 }
127 
128 impl TaskConfig {
satisfy_network(&self, network: &NetworkState) -> Result<(), Reason>129     pub(crate) fn satisfy_network(&self, network: &NetworkState) -> Result<(), Reason> {
130         // NetworkConfig::Cellular with NetworkType::Wifi is allowed
131         match network {
132             NetworkState::Offline => Err(Reason::NetworkOffline),
133             NetworkState::Online(info) => match self.common_data.network_config {
134                 NetworkConfig::Any => Ok(()),
135                 NetworkConfig::Wifi if info.network_type == NetworkType::Cellular => {
136                     Err(Reason::UnsupportedNetworkType)
137                 }
138                 _ => {
139                     if (self.common_data.roaming || !info.is_roaming)
140                         && (self.common_data.metered || !info.is_metered)
141                     {
142                         Ok(())
143                     } else {
144                         Err(Reason::UnsupportedNetworkType)
145                     }
146                 }
147             },
148         }
149     }
150 
satisfy_foreground(&self, foreground_abilities: &HashSet<u64>) -> bool151     pub(crate) fn satisfy_foreground(&self, foreground_abilities: &HashSet<u64>) -> bool {
152         self.common_data.mode == Mode::BackGround
153             || foreground_abilities.contains(&self.common_data.uid)
154     }
155 }
156 
157 pub(crate) struct ConfigSet {
158     pub(crate) headers: String,
159     pub(crate) extras: String,
160     pub(crate) form_items: Vec<CFormItem>,
161     pub(crate) file_specs: Vec<CFileSpec>,
162     pub(crate) body_file_names: Vec<CStringWrapper>,
163     pub(crate) certs_path: Vec<CStringWrapper>,
164 }
165 
166 impl PartialOrd for Mode {
partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering>167     fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
168         Some(self.cmp(other))
169     }
170 }
171 
172 impl Ord for Mode {
cmp(&self, other: &Self) -> std::cmp::Ordering173     fn cmp(&self, other: &Self) -> std::cmp::Ordering {
174         self.to_usize().cmp(&other.to_usize())
175     }
176 }
177 
178 impl Mode {
to_usize(self) -> usize179     fn to_usize(self) -> usize {
180         match self {
181             Mode::FrontEnd => 0,
182             Mode::Any => 1,
183             Mode::BackGround => 2,
184             _ => unreachable!(),
185         }
186     }
187 }
188 
189 impl From<u8> for Mode {
from(value: u8) -> Self190     fn from(value: u8) -> Self {
191         match value {
192             0 => Mode::BackGround,
193             1 => Mode::FrontEnd,
194             _ => Mode::Any,
195         }
196     }
197 }
198 
199 impl From<u8> for Action {
from(value: u8) -> Self200     fn from(value: u8) -> Self {
201         match value {
202             0 => Action::Download,
203             1 => Action::Upload,
204             _ => Action::Any,
205         }
206     }
207 }
208 
209 impl From<u8> for Version {
from(value: u8) -> Self210     fn from(value: u8) -> Self {
211         match value {
212             2 => Version::API10,
213             _ => Version::API9,
214         }
215     }
216 }
217 
218 impl From<u8> for NetworkConfig {
from(value: u8) -> Self219     fn from(value: u8) -> Self {
220         match value {
221             0 => NetworkConfig::Any,
222             2 => NetworkConfig::Cellular,
223             _ => NetworkConfig::Wifi,
224         }
225     }
226 }
227 
228 impl TaskConfig {
build_config_set(&self) -> ConfigSet229     pub(crate) fn build_config_set(&self) -> ConfigSet {
230         ConfigSet {
231             headers: hashmap_to_string(&self.headers),
232             extras: hashmap_to_string(&self.extras),
233             form_items: self.form_items.iter().map(|x| x.to_c_struct()).collect(),
234             file_specs: self.file_specs.iter().map(|x| x.to_c_struct()).collect(),
235             body_file_names: self
236                 .body_file_paths
237                 .iter()
238                 .map(CStringWrapper::from)
239                 .collect(),
240             certs_path: self.certs_path.iter().map(CStringWrapper::from).collect(),
241         }
242     }
243 
contains_user_file(&self) -> bool244     pub(crate) fn contains_user_file(&self) -> bool {
245         for specs in self.file_specs.iter() {
246             if specs.is_user_file {
247                 return true;
248             }
249         }
250         false
251     }
252 }
253 
254 impl Default for TaskConfig {
default() -> Self255     fn default() -> Self {
256         Self {
257             bundle_type: 0,
258             atomic_account: "ohosAnonymousUid".to_string(),
259             bundle: "xxx".to_string(),
260             url: "".to_string(),
261             title: "xxx".to_string(),
262             description: "xxx".to_string(),
263             method: "GET".to_string(),
264             headers: Default::default(),
265             data: "".to_string(),
266             token: "xxx".to_string(),
267             proxy: "".to_string(),
268             extras: Default::default(),
269             version: Version::API10,
270             form_items: vec![],
271             file_specs: vec![],
272             body_file_paths: vec![],
273             certs_path: vec![],
274             certificate_pins: "".to_string(),
275             common_data: CommonTaskConfig {
276                 task_id: 0,
277                 uid: 0,
278                 token_id: 0,
279                 action: Action::Download,
280                 mode: Mode::BackGround,
281                 cover: false,
282                 network_config: NetworkConfig::Any,
283                 metered: false,
284                 roaming: false,
285                 retry: false,
286                 redirect: true,
287                 index: 0,
288                 begins: 0,
289                 ends: -1,
290                 gauge: false,
291                 precise: false,
292                 priority: 0,
293                 background: false,
294                 multipart: false,
295             },
296         }
297     }
298 }
299 
300 /// ConfigBuilder
301 pub struct ConfigBuilder {
302     inner: TaskConfig,
303 }
304 
305 impl ConfigBuilder {
306     /// Create a new ConfigBuilder
new() -> Self307     pub fn new() -> Self {
308         Self {
309             inner: TaskConfig::default(),
310         }
311     }
312     /// Set url
url(&mut self, url: &str) -> &mut Self313     pub fn url(&mut self, url: &str) -> &mut Self {
314         self.inner.url = url.to_string();
315         self
316     }
317 
318     /// set version
version(&mut self, version: u8) -> &mut Self319     pub fn version(&mut self, version: u8) -> &mut Self {
320         self.inner.version = version.into();
321         self
322     }
323 
324     /// Set title
file_spec(&mut self, file: File) -> &mut Self325     pub fn file_spec(&mut self, file: File) -> &mut Self {
326         self.inner.file_specs.push(FileSpec::user_file(file));
327         self
328     }
329     /// Set action
action(&mut self, action: Action) -> &mut Self330     pub fn action(&mut self, action: Action) -> &mut Self {
331         self.inner.common_data.action = action;
332         self
333     }
334 
335     /// Set mode
mode(&mut self, mode: Mode) -> &mut Self336     pub fn mode(&mut self, mode: Mode) -> &mut Self {
337         self.inner.common_data.mode = mode;
338         self
339     }
340 
341     /// Set bundle name
bundle_name(&mut self, bundle_name: &str) -> &mut Self342     pub fn bundle_name(&mut self, bundle_name: &str) -> &mut Self {
343         self.inner.bundle = bundle_name.to_string();
344         self
345     }
346 
347     /// Set uid
uid(&mut self, uid: u64) -> &mut Self348     pub fn uid(&mut self, uid: u64) -> &mut Self {
349         self.inner.common_data.uid = uid;
350         self
351     }
352 
353     /// set network
network(&mut self, network: NetworkConfig) -> &mut Self354     pub fn network(&mut self, network: NetworkConfig) -> &mut Self {
355         self.inner.common_data.network_config = network;
356         self
357     }
358 
359     /// Set metered
roaming(&mut self, roaming: bool) -> &mut Self360     pub fn roaming(&mut self, roaming: bool) -> &mut Self {
361         self.inner.common_data.roaming = roaming;
362         self
363     }
364 
365     /// set metered
metered(&mut self, metered: bool) -> &mut Self366     pub fn metered(&mut self, metered: bool) -> &mut Self {
367         self.inner.common_data.metered = metered;
368         self
369     }
370 
371     /// build
build(&mut self) -> TaskConfig372     pub fn build(&mut self) -> TaskConfig {
373         self.inner.clone()
374     }
375 
376     /// redirect
redirect(&mut self, redirect: bool) -> &mut Self377     pub fn redirect(&mut self, redirect: bool) -> &mut Self {
378         self.inner.common_data.redirect = redirect;
379         self
380     }
381 
382     /// begins
begins(&mut self, begins: u64) -> &mut Self383     pub fn begins(&mut self, begins: u64) -> &mut Self {
384         self.inner.common_data.begins = begins;
385         self
386     }
387 
388     /// ends
ends(&mut self, ends: u64) -> &mut Self389     pub fn ends(&mut self, ends: u64) -> &mut Self {
390         self.inner.common_data.ends = ends as i64;
391         self
392     }
393 
394     /// method
method(&mut self, metered: &str) -> &mut Self395     pub fn method(&mut self, metered: &str) -> &mut Self {
396         self.inner.method = metered.to_string();
397         self
398     }
399 
400     /// retry
retry(&mut self, retry: bool) -> &mut Self401     pub fn retry(&mut self, retry: bool) -> &mut Self {
402         self.inner.common_data.retry = retry;
403         self
404     }
405 }
406 
407 #[cfg(feature = "oh")]
408 impl Serialize for TaskConfig {
serialize(&self, parcel: &mut ipc::parcel::MsgParcel) -> ipc::IpcResult<()>409     fn serialize(&self, parcel: &mut ipc::parcel::MsgParcel) -> ipc::IpcResult<()> {
410         parcel.write(&(self.common_data.action.repr as u32))?;
411         parcel.write(&(self.version as u32))?;
412         parcel.write(&(self.common_data.mode.repr as u32))?;
413         parcel.write(&self.bundle_type)?;
414         parcel.write(&self.common_data.cover)?;
415         parcel.write(&(self.common_data.network_config as u32))?;
416         parcel.write(&(self.common_data.metered))?;
417         parcel.write(&self.common_data.roaming)?;
418         parcel.write(&(self.common_data.retry))?;
419         parcel.write(&(self.common_data.redirect))?;
420         parcel.write(&(self.common_data.background))?;
421         parcel.write(&(self.common_data.multipart))?;
422         parcel.write(&self.common_data.index)?;
423         parcel.write(&(self.common_data.begins as i64))?;
424         parcel.write(&self.common_data.ends)?;
425         parcel.write(&self.common_data.gauge)?;
426         parcel.write(&self.common_data.precise)?;
427         parcel.write(&self.common_data.priority)?;
428         parcel.write(&self.url)?;
429         parcel.write(&self.title)?;
430         parcel.write(&self.method)?;
431         parcel.write(&self.token)?;
432         parcel.write(&self.description)?;
433         parcel.write(&self.data)?;
434         parcel.write(&self.proxy)?;
435         parcel.write(&self.certificate_pins)?;
436 
437         parcel.write(&(self.certs_path.len() as u32))?;
438         for cert_path in &self.certs_path {
439             parcel.write(cert_path)?;
440         }
441 
442         parcel.write(&(self.form_items.len() as u32))?;
443         for form_item in &self.form_items {
444             parcel.write(&form_item.name)?;
445             parcel.write(&form_item.value)?;
446         }
447         parcel.write(&(self.file_specs.len() as u32))?;
448         for file_spec in &self.file_specs {
449             parcel.write(&file_spec.name)?;
450             parcel.write(&file_spec.path)?;
451             parcel.write(&file_spec.file_name)?;
452             parcel.write(&file_spec.mime_type)?;
453             parcel.write(&file_spec.is_user_file)?;
454             if file_spec.is_user_file {
455                 let file = unsafe { File::from_raw_fd(file_spec.fd.unwrap()) };
456                 parcel.write_file(file)?;
457             }
458         }
459 
460         parcel.write(&(self.body_file_paths.len() as u32))?;
461         for body_file_paths in self.body_file_paths.iter() {
462             parcel.write(body_file_paths)?;
463         }
464         parcel.write(&(self.headers.len() as u32))?;
465         for header in self.headers.iter() {
466             parcel.write(header.0)?;
467             parcel.write(header.1)?;
468         }
469 
470         parcel.write(&(self.extras.len() as u32))?;
471         for extra in self.extras.iter() {
472             parcel.write(extra.0)?;
473             parcel.write(extra.1)?;
474         }
475 
476         Ok(())
477     }
478 }
479 
480 #[cfg(feature = "oh")]
481 impl Deserialize for TaskConfig {
deserialize(parcel: &mut ipc::parcel::MsgParcel) -> ipc::IpcResult<Self>482     fn deserialize(parcel: &mut ipc::parcel::MsgParcel) -> ipc::IpcResult<Self> {
483         let action: u32 = parcel.read()?;
484         let action: Action = Action::from(action as u8);
485         let version: u32 = parcel.read()?;
486         let version: Version = Version::from(version as u8);
487         let mode: u32 = parcel.read()?;
488         let mode: Mode = Mode::from(mode as u8);
489         let bundle_type: u32 = parcel.read()?;
490         let cover: bool = parcel.read()?;
491         let network: u32 = parcel.read()?;
492         let network_config = NetworkConfig::from(network as u8);
493         let metered: bool = parcel.read()?;
494         let roaming: bool = parcel.read()?;
495         let retry: bool = parcel.read()?;
496         let redirect: bool = parcel.read()?;
497         let background: bool = parcel.read()?;
498         let multipart: bool = parcel.read()?;
499         let index: u32 = parcel.read()?;
500         let begins: i64 = parcel.read()?;
501         let ends: i64 = parcel.read()?;
502         let gauge: bool = parcel.read()?;
503         let precise: bool = parcel.read()?;
504         let priority: u32 = parcel.read()?;
505         let url: String = parcel.read()?;
506         let title: String = parcel.read()?;
507         let method: String = parcel.read()?;
508         let token: String = parcel.read()?;
509         let description: String = parcel.read()?;
510         let data_base: String = parcel.read()?;
511         let proxy: String = parcel.read()?;
512         let certificate_pins: String = parcel.read()?;
513         let bundle = query_calling_bundle();
514         let uid = ipc::Skeleton::calling_uid();
515         let token_id = ipc::Skeleton::calling_full_token_id();
516         let certs_path_size: u32 = parcel.read()?;
517         if certs_path_size > parcel.readable() as u32 {
518             error!("deserialize failed: certs_path_size too large");
519             sys_event!(
520                 ExecFault,
521                 DfxCode::INVALID_IPC_MESSAGE_A00,
522                 "deserialize failed: certs_path_size too large"
523             );
524             return Err(IpcStatusCode::Failed);
525         }
526         let mut certs_path = Vec::new();
527         for _ in 0..certs_path_size {
528             let cert_path: String = parcel.read()?;
529             certs_path.push(cert_path);
530         }
531 
532         let form_size: u32 = parcel.read()?;
533         if form_size > parcel.readable() as u32 {
534             error!("deserialize failed: form_size too large");
535             sys_event!(
536                 ExecFault,
537                 DfxCode::INVALID_IPC_MESSAGE_A00,
538                 "deserialize failed: form_size too large"
539             );
540             return Err(IpcStatusCode::Failed);
541         }
542         let mut form_items = Vec::new();
543         for _ in 0..form_size {
544             let name: String = parcel.read()?;
545             let value: String = parcel.read()?;
546             form_items.push(FormItem { name, value });
547         }
548 
549         let file_size: u32 = parcel.read()?;
550         if file_size > parcel.readable() as u32 {
551             error!("deserialize failed: file_specs size too large");
552             sys_event!(
553                 ExecFault,
554                 DfxCode::INVALID_IPC_MESSAGE_A00,
555                 "deserialize failed: file_specs size too large"
556             );
557             return Err(IpcStatusCode::Failed);
558         }
559         let mut file_specs: Vec<FileSpec> = Vec::new();
560         for _ in 0..file_size {
561             let name: String = parcel.read()?;
562             let path: String = parcel.read()?;
563             let file_name: String = parcel.read()?;
564             let mime_type: String = parcel.read()?;
565             let is_user_file: bool = parcel.read()?;
566             let mut fd: Option<RawFd> = None;
567             if is_user_file {
568                 let ipc_fd: File = parcel.read_file()?;
569                 fd = Some(ipc_fd.into_raw_fd());
570             }
571             file_specs.push(FileSpec {
572                 name,
573                 path,
574                 file_name,
575                 mime_type,
576                 is_user_file,
577                 fd,
578             });
579         }
580 
581         // Response bodies fd.
582         let body_file_size: u32 = parcel.read()?;
583         if body_file_size > parcel.readable() as u32 {
584             error!("deserialize failed: body_file size too large");
585             sys_event!(
586                 ExecFault,
587                 DfxCode::INVALID_IPC_MESSAGE_A00,
588                 "deserialize failed: body_file size too large"
589             );
590             return Err(IpcStatusCode::Failed);
591         }
592 
593         let mut body_file_paths: Vec<String> = Vec::new();
594         for _ in 0..body_file_size {
595             let file_name: String = parcel.read()?;
596             body_file_paths.push(file_name);
597         }
598 
599         let header_size: u32 = parcel.read()?;
600         if header_size > parcel.readable() as u32 {
601             error!("deserialize failed: header size too large");
602             sys_event!(
603                 ExecFault,
604                 DfxCode::INVALID_IPC_MESSAGE_A00,
605                 "deserialize failed: header size too large"
606             );
607             return Err(IpcStatusCode::Failed);
608         }
609         let mut headers: HashMap<String, String> = HashMap::new();
610         for _ in 0..header_size {
611             let key: String = parcel.read()?;
612             let value: String = parcel.read()?;
613             headers.insert(key, value);
614         }
615 
616         let extras_size: u32 = parcel.read()?;
617         if extras_size > parcel.readable() as u32 {
618             error!("deserialize failed: extras size too large");
619             sys_event!(
620                 ExecFault,
621                 DfxCode::INVALID_IPC_MESSAGE_A00,
622                 "deserialize failed: extras size too large"
623             );
624             return Err(IpcStatusCode::Failed);
625         }
626         let mut extras: HashMap<String, String> = HashMap::new();
627         for _ in 0..extras_size {
628             let key: String = parcel.read()?;
629             let value: String = parcel.read()?;
630             extras.insert(key, value);
631         }
632 
633         let atomic_account = if bundle_type == ATOMIC_SERVICE {
634             GetOhosAccountUid()
635         } else {
636             "".to_string()
637         };
638 
639         let task_config = TaskConfig {
640             bundle,
641             bundle_type,
642             atomic_account,
643             url,
644             title,
645             description,
646             method,
647             headers,
648             data: data_base,
649             token,
650             proxy,
651             certificate_pins,
652             extras,
653             version,
654             form_items,
655             file_specs,
656             body_file_paths,
657             certs_path,
658             common_data: CommonTaskConfig {
659                 task_id: 0,
660                 uid,
661                 token_id,
662                 action,
663                 mode,
664                 cover,
665                 network_config,
666                 metered,
667                 roaming,
668                 retry,
669                 redirect,
670                 index,
671                 begins: begins as u64,
672                 ends,
673                 gauge,
674                 precise,
675                 priority,
676                 background,
677                 multipart,
678             },
679         };
680         Ok(task_config)
681     }
682 }
683 
684 #[cfg(test)]
685 mod test {
686     use super::*;
687     #[test]
ut_enum_action()688     fn ut_enum_action() {
689         assert_eq!(Action::Download.repr, 0);
690         assert_eq!(Action::Upload.repr, 1);
691         assert_eq!(Action::Any.repr, 2);
692     }
693 
694     #[test]
ut_enum_mode()695     fn ut_enum_mode() {
696         assert_eq!(Mode::BackGround.repr, 0);
697         assert_eq!(Mode::FrontEnd.repr, 1);
698         assert_eq!(Mode::Any.repr, 2);
699     }
700 
701     #[test]
ut_enum_version()702     fn ut_enum_version() {
703         assert_eq!(Version::API9 as u32, 1);
704         assert_eq!(Version::API10 as u32, 2);
705     }
706 
707     #[test]
ut_enum_network_config()708     fn ut_enum_network_config() {
709         assert_eq!(NetworkConfig::Any as u32, 0);
710         assert_eq!(NetworkConfig::Wifi as u32, 1);
711         assert_eq!(NetworkConfig::Cellular as u32, 2);
712     }
713 }
714