dji_log_parser/lib.rs
1//! # DJILog Parser Module
2//!
3//! This module provides functionality for parsing DJI log files.
4//!
5//! ## Encryption in Version 13 and Later
6//! Starting with version 13, log records are AES encrypted and require a specific keychain
7//! for decryption. This keychain must be obtained from DJI using their API. An apiKey is
8//! necessary to access the DJI API.
9//!
10//! Once keychains are retrieved, they can be stored along with the original log for further
11//! offline use.
12//!
13//! ### Obtaining an ApiKey
14//! To acquire an apiKey, follow these steps:
15//! 1. Visit [DJI Developer Technologies](https://developer.dji.com/user) and log in.
16//! 2. Click `CREATE APP`, choose `Open API` as the App Type, and provide the necessary
17//!    details like `App Name`, `Category`, and `Description`.
18//! 3. After creating the app, activate it through the link sent to your email.
19//! 4. On your developer user page, find your app's details to retrieve the ApiKey
20//!    (labeled as the SDK key).
21//!
22//! ## Usage
23//!
24//! ### Initialization
25//! Initialize a `DJILog` instance from a byte slice to access version information and metadata:
26//! ```
27//! let parser = DJILog::from_bytes(bytes).unwrap();
28//! ```
29//!
30//! ### Access general data
31//!
32//! General data are not encrypted and can be accessed from the parser for all log versions:
33//!
34//! ```
35//! // Print the log version
36//! println!("Version: {:?}", parser.version);
37//!
38//! // Print the log details section
39//! println!("Details: {}", parser.details);
40//! ```
41//!
42//! ### Retrieve keychains
43//!
44//! For logs version 13 and later, keychains must be retrieved from the DJI API to decode the records:
45//!
46//! ```
47//! // Replace `__DJI_API_KEY__` with your actual apiKey
48//! let keychains = parser.fetch_keychains("__DJI_API_KEY__").unwrap();
49//! ```
50//!
51//! Keychains can be retrieved once, serialized, and stored along with the log file for future offline use.
52//!
53//! ### Accessing Frames
54//!
55//! Decrypt frames based on the log file version.
56//!
57//! A `Frame` is a standardized representation of log data, normalized across different log versions.
58//! It provides a consistent and easy-to-use format for analyzing and processing DJI log information.
59//!
60//! For versions prior to 13:
61//!
62//! ```
63//! let frames = parser.frames(None);
64//! ```
65//!
66//! For version 13 and later:
67//!
68//! ```
69//! let frames = parser.frames(Some(keychains));
70//! ```
71//!
72//! ### Accessing raw Records
73//!
74//! Decrypt raw records based on the log file version.
75//! For versions prior to 13:
76//!
77//! ```
78//! let records = parser.records(None);
79//! ```
80//!
81//! For version 13 and later:
82//!
83//! ```
84//! let records = parser.records(Some(keychains));
85//! ```
86//!
87//!
88//! ## Binary structure of log files:
89//!
90//! v1 -> v6
91//! ```text
92//! ┌─────────────────┐
93//! │     Prefix      │ detail_offset ─┐
94//! ├─────────────────┤                │
95//! │     Records     │                │
96//! ├─────────────────┤<───────────────┘
97//! │     Details     │ detail_length
98//! └─────────────────┘
99//! ```
100//!
101//! v7 -> v11
102//! ```text
103//! ┌─────────────────┐
104//! │     Prefix      │ detail_offset ─┐
105//! ├─────────────────┤                │
106//! │     Records     │                │
107//! │   (Encrypted)   │                |
108//! ├─────────────────┤<───────────────┘
109//! │     Details     │ detail_length
110//! └─────────────────┘
111//!```
112//!
113//! v12
114//! ```text
115//! ┌─────────────────┐
116//! │     Prefix      │ detail_offset ─┐
117//! ├─────────────────┤                │
118//! │      Details    │ detail_length  │
119//! ├─────────────────┤                │
120//! │     Records     │                │
121//! │   (Encrypted)   │                │
122//! └─────────────────┘<───────────────┘
123//!```
124//!
125//! v13 -> v14
126//! ```text
127//! ┌─────────────────┐
128//! │     Prefix      │ detail_offset ─┐
129//! ├─────────────────┤                │
130//! │ Auxiliary Info  |                |
131//! │ (Encrypted      │ detail_length  │
132//! │      Details)   |                |
133//! ├─────────────────┤                │
134//! │    Auxiliary    |                |
135//! │     Version     |                │
136//! ├─────────────────┤<───────────────┘
137//! │     Records     │
138//! │(Encrypted + AES)|
139//! └─────────────────┘
140//! ```
141use base64::engine::general_purpose::STANDARD as Base64Standard;
142use base64::Engine as _;
143use binrw::io::Cursor;
144use binrw::BinRead;
145use std::cell::RefCell;
146use std::collections::VecDeque;
147
148mod decoder;
149mod error;
150pub mod frame;
151pub mod keychain;
152pub mod layout;
153pub mod record;
154mod utils;
155
156pub use error::{Error, Result};
157use frame::{records_to_frames, Frame};
158use keychain::{EncodedKeychainFeaturePoint, Keychain, KeychainFeaturePoint, KeychainsRequest};
159use layout::auxiliary::{Auxiliary, Department};
160use layout::details::Details;
161use layout::prefix::Prefix;
162use record::Record;
163
164use crate::utils::pad_with_zeros;
165
166#[derive(Debug)]
167pub struct DJILog {
168    inner: Vec<u8>,
169    prefix: Prefix,
170    /// Log format version
171    pub version: u8,
172    /// Log Details. Contains record summary and general informations
173    pub details: Details,
174}
175
176impl DJILog {
177    /// Constructs a `DJILog` from an array of bytes.
178    ///
179    /// This function parses the Prefix and Info blocks of the log file,
180    /// and handles different versions of the log format.
181    ///
182    /// # Arguments
183    ///
184    /// * `bytes` - An array of bytes representing the DJI log file.
185    ///
186    /// # Returns
187    ///
188    /// This function returns `Result<DJILog>`.
189    /// On success, it returns the `DJILog` instance.
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// use djilog_parser::DJILog;
195    ///
196    /// let log_bytes = include_bytes!("path/to/log/file");
197    /// let log = DJILog::from_bytes(log_bytes).unwrap();
198    /// ```
199    ///
200    pub fn from_bytes(bytes: Vec<u8>) -> Result<DJILog> {
201        // Decode Prefix
202        let mut prefix = Prefix::read(&mut Cursor::new(&bytes))?;
203
204        let version = prefix.version;
205
206        // Decode Detail
207        let detail_offset = prefix.detail_offset() as usize;
208        let mut cursor = Cursor::new(pad_with_zeros(&bytes[detail_offset..], 400));
209
210        let details = if version < 13 {
211            Details::read_args(&mut cursor, (version,))?
212        } else {
213            // Get details from first auxiliary block
214            if let Auxiliary::Info(data) = Auxiliary::read(&mut cursor)? {
215                Details::read_args(&mut Cursor::new(&data.info_data), (version,))?
216            } else {
217                return Err(Error::MissingAuxilliaryData("Info".into()));
218            }
219        };
220
221        // Try to recover detail offset
222        if prefix.records_offset() == 0 && version >= 13 {
223            // Skip second auxiliary block
224            let _ = Auxiliary::read(&mut cursor)?;
225            prefix.recover_detail_offset(cursor.position() + detail_offset as u64);
226        }
227
228        Ok(DJILog {
229            inner: bytes,
230            prefix,
231            version,
232            details,
233        })
234    }
235
236    /// Creates a `KeychainsRequest` object by parsing `KeyStorage` records.
237    ///
238    /// This function is used to build a request body for manually retrieving the keychain from the DJI API.
239    /// Keychains are required to decode records for logs with a version greater than or equal to 13.
240    /// For earlier versions, this function returns a default `KeychainsRequest`.
241    ///
242    /// # Returns
243    ///
244    /// Returns a `Result<KeychainsRequest>`. On success, it provides a `KeychainsRequest`
245    /// instance, which contains the necessary information to fetch keychains from the DJI API.
246    ///
247    pub fn keychains_request(&self) -> Result<KeychainsRequest> {
248        self.keychains_request_with_custom_params(None, None)
249    }
250
251    /// Creates a `KeychainsRequest` object by parsing `KeyStorage` records with manually specified params.
252    ///
253    /// This function is used to build a request body for manually retrieving the keychain from the DJI API.
254    /// Keychains are required to decode records for logs with a version greater than or equal to 13.
255    /// For earlier versions, this function returns a default `KeychainsRequest`.
256    ///
257    /// # Arguments
258    ///
259    /// * `department` - An optional `Department` to manually set in the request. If `None`, the department
260    ///   will be determined from the log file.
261    /// * `version` - An optional version number to manually set in the request. If `None`, the version
262    ///   will be determined from the log file.
263    ///
264    /// # Returns
265    ///
266    /// Returns a `Result<KeychainsRequest>`. On success, it provides a `KeychainsRequest`
267    /// instance, which contains the necessary information to fetch keychains from the DJI API.
268    ///
269    pub fn keychains_request_with_custom_params(
270        &self,
271        department: Option<Department>,
272        version: Option<u16>,
273    ) -> Result<KeychainsRequest> {
274        let mut keychain_request = KeychainsRequest::default();
275
276        // No keychain
277        if self.version < 13 {
278            return Ok(keychain_request);
279        }
280
281        let mut cursor = Cursor::new(&self.inner);
282        cursor.set_position(self.prefix.detail_offset());
283
284        // Skip first auxiliary block
285        let _ = Auxiliary::read(&mut cursor)?;
286
287        // Get version from second auxilliary block
288        if let Auxiliary::Version(data) = Auxiliary::read(&mut cursor)? {
289            // Use provided version or determine from log
290            keychain_request.version = version.unwrap_or(data.version);
291            // Use provided department or determine from log
292            keychain_request.department = match department {
293                Some(dept) => dept.into(),
294                None => match data.department {
295                    Department::Unknown(_) => Department::DJIFly.into(),
296                    _ => data.department.into(),
297                },
298            };
299        } else {
300            return Err(Error::MissingAuxilliaryData("Version".into()));
301        }
302
303        // Extract keychains from KeyStorage Records
304        cursor.set_position(self.prefix.records_offset());
305
306        let mut keychain: Vec<EncodedKeychainFeaturePoint> = Vec::new();
307
308        while cursor.position() < self.prefix.records_end_offset(self.inner.len() as u64) {
309            let empty_keychain = &RefCell::new(Keychain::empty());
310            let record = match Record::read_args(
311                &mut cursor,
312                binrw::args! {
313                    version: self.version,
314                    keychain: empty_keychain
315                },
316            ) {
317                Ok(record) => record,
318                Err(_) => break,
319            };
320
321            match record {
322                Record::KeyStorage(data) => {
323                    // add EncodedKeychainFeaturePoint to current keychain
324                    keychain.push(EncodedKeychainFeaturePoint {
325                        feature_point: data.feature_point,
326                        aes_ciphertext: Base64Standard.encode(&data.data),
327                    });
328                }
329                Record::KeyStorageRecover(_) => {
330                    // start a new keychain
331                    keychain_request.keychains.push(keychain);
332                    keychain = Vec::new();
333                }
334                _ => {}
335            }
336        }
337
338        keychain_request.keychains.push(keychain);
339
340        Ok(keychain_request)
341    }
342
343    /// Fetches keychains using the provided API key.
344    ///
345    /// This function first creates a `KeychainRequest` using the `keychain_request()` method,
346    /// then uses that request to fetch the actual keychains from the DJI API.
347    /// Keychains are required to decode records for logs with a version greater than or equal to 13.
348    ///
349    /// # Arguments
350    ///
351    /// * `api_key` - A string slice that holds the API key for authentication with the DJI API.
352    ///
353    /// # Returns
354    ///
355    /// Returns a `Result<Vec<Vec<KeychainFeaturePoint>>>`. On success, it provides a vector of vectors,
356    /// where each inner vector represents a keychain.
357    ///
358    #[cfg(not(target_arch = "wasm32"))]
359    pub fn fetch_keychains(&self, api_key: &str) -> Result<Vec<Vec<KeychainFeaturePoint>>> {
360        if self.version >= 13 {
361            self.keychains_request()?.fetch(api_key, None)
362        } else {
363            Ok(Vec::new())
364        }
365    }
366
367    /// Fetches keychains asynchronously using the provided API key.
368    /// Available on wasm and native behind the `native-async` feature.
369    ///
370    /// This function first creates a `KeychainRequest` using the `keychain_request()` method,
371    /// then uses that request to asynchronously fetch the actual keychains from the DJI API.
372    /// Keychains are required to decode records for logs with a version greater than or equal to 13.
373    ///
374    /// # Arguments
375    ///
376    /// * `api_key` - A string slice that holds the API key for authentication with the DJI API.
377    ///
378    /// # Returns
379    ///
380    /// Returns a `Result<Vec<Vec<KeychainFeaturePoint>>>`. On success, it provides a vector of vectors,
381    /// where each inner vector represents a keychain.
382    ///
383    #[cfg(any(target_arch = "wasm32", feature = "native-async"))]
384    pub async fn fetch_keychains_async(
385        &self,
386        api_key: &str,
387    ) -> Result<Vec<Vec<KeychainFeaturePoint>>> {
388        if self.version >= 13 {
389            self.keychains_request()?.fetch_async(api_key, None).await
390        } else {
391            Ok(Vec::new())
392        }
393    }
394
395    /// Retrieves the parsed raw records from the DJI log.
396    ///
397    /// This function decodes the raw records from the log file
398    ///
399    /// # Arguments
400    ///
401    /// * `keychains` - An optional vector of vectors containing `KeychainFeaturePoint` instances. This parameter
402    ///   is used for decryption when working with encrypted logs (versions >= 13). If `None` is provided,
403    ///   the function will attempt to process the log without decryption.
404    ///
405    ///
406    /// # Returns
407    ///
408    /// Returns a `Result<Vec<Record>>`. On success, it provides a vector of `Record`
409    /// instances representing the parsed log records.
410    ///
411    pub fn records(
412        &self,
413        keychains: Option<Vec<Vec<KeychainFeaturePoint>>>,
414    ) -> Result<Vec<Record>> {
415        if self.version >= 13 && keychains.is_none() {
416            return Err(Error::KeychainRequired);
417        }
418
419        let mut keychains = VecDeque::from(match keychains {
420            Some(keychains) => keychains
421                .iter()
422                .map(Keychain::from_feature_points)
423                .collect(),
424            None => Vec::new(),
425        });
426
427        let mut cursor = Cursor::new(&self.inner);
428        cursor.set_position(self.prefix.records_offset());
429
430        let mut keychain = RefCell::new(keychains.pop_front().unwrap_or(Keychain::empty()));
431
432        let mut records = Vec::new();
433
434        while cursor.position() < self.prefix.records_end_offset(self.inner.len() as u64) {
435            // decode record
436            let record = match Record::read_args(
437                &mut cursor,
438                binrw::args! {
439                    version: self.version,
440                    keychain: &keychain
441                },
442            ) {
443                Ok(record) => record,
444                Err(_) => break,
445            };
446
447            if let Record::KeyStorageRecover(_) = record {
448                keychain = RefCell::new(keychains.pop_front().unwrap_or(Keychain::empty()));
449            }
450
451            records.push(record);
452        }
453
454        Ok(records)
455    }
456
457    /// Retrieves the normalized frames from the DJI log.
458    ///
459    /// This function processes the raw records from the log file and converts them into standardized
460    /// frames. Frames are a more user-friendly representation of the log data, normalized across all
461    /// log versions for easier use and analysis.
462    ///
463    /// The function first decodes the raw records based on the specified decryption method, then
464    /// converts these records into frames. This normalization process makes it easier to work with
465    /// log data from different DJI log versions.
466    ///
467    /// # Arguments
468    ///
469    /// * `keychains` - An optional vector of vectors containing `KeychainFeaturePoint` instances. This parameter
470    ///   is used for decryption when working with encrypted logs (versions >= 13). If `None` is provided,
471    ///   the function will attempt to process the log without decryption.
472    ///
473    ///
474    /// # Returns
475    ///
476    /// Returns a `Result<Vec<Frame>>`. On success, it provides a vector of `Frame`
477    /// instances representing the normalized log data.
478    ///
479    /// # Note
480    ///
481    /// This method consumes and processes the raw records to create frames. It's generally preferred
482    /// over using raw records directly, as frames provide a consistent format across different log
483    /// versions, simplifying data analysis and interpretation.
484    ///
485    pub fn frames(&self, keychains: Option<Vec<Vec<KeychainFeaturePoint>>>) -> Result<Vec<Frame>> {
486        let records = self.records(keychains)?;
487        Ok(records_to_frames(records, self.details.clone()))
488    }
489}