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}