pdf_perm/
lib.rs

1//! # `pdf-perm` library crate
2//!
3//! If you are reading this, you are reading the documentation for the `pdf-perm` library crate. For the cli, kindly refer to the README file.
4//!
5//! This library crate provides several traits related to [`Permissions`].
6
7#![deny(missing_docs)]
8#![warn(clippy::all, clippy::nursery, clippy::pedantic, clippy::cargo)]
9
10use bitflags::Flags;
11use log::{debug, error, warn};
12use lopdf::{
13    Document, EncryptionState, EncryptionVersion, Error, Permissions, Result as PdfResult,
14};
15
16/// Trait for non-encrypted PDF [`Document`]s, allowing for easy getting and setting of [`Permissions`].
17pub trait PdfPerm {
18    /// Returns the permissions of the PDF document.
19    fn permissions(&self) -> Permissions;
20    /// Sets the permissions of the PDF document.
21    ///
22    /// # Note
23    ///
24    /// You must save the document for the permissions to take effect. See [`Document::save`] and [`Document::save_to`].
25    ///
26    /// # Errors
27    ///
28    /// Returns [`Error::AlreadyEncrypted`] if the document is encrypted, or other variants if operation fails.
29    fn set_permissions(&mut self, permissions: Permissions) -> PdfResult<()>;
30}
31
32impl PdfPerm for Document {
33    fn permissions(&self) -> Permissions {
34        self.encryption_state
35            .as_ref()
36            .map(EncryptionState::permissions)
37            .unwrap_or_default()
38    }
39    fn set_permissions(&mut self, permissions: Permissions) -> PdfResult<()> {
40        if self.is_encrypted() {
41            error!("Does not support setting permissions on encrypted documents");
42            return Err(Error::AlreadyEncrypted);
43        }
44        let version = EncryptionVersion::V1 {
45            document: self,
46            owner_password: "",
47            user_password: "",
48            permissions,
49        };
50        let state: EncryptionState = version.try_into()?;
51        debug!("Encryption state: {state:?}");
52        self.encrypt(&state)?;
53
54        Ok(())
55    }
56}
57
58// TODO: Decoupling `ShortFlags` into a separate crate `short-flags`?
59/// Trait for [`Flags`] to provide short flag functionality.
60pub trait ShortFlags: Flags + Copy {
61    // Required constant
62    /// The set of defined short flags. Must be of the same length as [`Flags::FLAGS`].
63    const SHORT_FLAGS: &'static [char];
64
65    // Provided methods
66    /// Parses a character into a [`Flags`].
67    fn from_char(c: char) -> Option<Self> {
68        if c == '*' {
69            return Some(Self::all());
70        }
71        let index = Self::SHORT_FLAGS.iter().position(|&flag| flag == c)?;
72        Some(*Self::FLAGS.get(index)?.value())
73    }
74    /// Parses a string into self, with given short flags.
75    #[must_use]
76    fn from_str(s: &str) -> Self {
77        let mut flags = Self::empty();
78        for c in s.chars() {
79            if let Some(flag) = Self::from_char(c) {
80                flags.insert(flag);
81            } else {
82                warn!("Invalid permission character: {c}");
83            }
84        }
85        flags
86    }
87    /// Applies the given modification string.
88    fn apply_modification(&mut self, modification: &str) {
89        let (first, rest) = modification.split_at(1);
90        let flags_mod = Self::from_str(rest);
91        match first {
92            "+" => self.insert(flags_mod), // Set given flags
93            "-" => self.remove(flags_mod), // Unset given flags
94            "=" => *self = flags_mod,      // Set to given flags
95            _ => warn!("Invalid modification: {modification}"),
96        }
97    }
98    /// Returns a concise summary of the flags, with short flag for set flags and `-` for unset flags.
99    fn summary(&self) -> String {
100        let mut summary = String::with_capacity(Self::SHORT_FLAGS.len());
101        for (short, flag) in Self::SHORT_FLAGS.iter().zip(Self::FLAGS) {
102            if self.contains(*flag.value()) {
103                summary.push(*short);
104            } else {
105                summary.push('-');
106            }
107        }
108        summary
109    }
110}
111
112impl ShortFlags for Permissions {
113    const SHORT_FLAGS: &'static [char] = &['p', 'm', 'c', 'a', 'f', 'x', 's', 'q'];
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use lopdf::Object;
120
121    // Test `PdfPerm` trait
122    fn create_test_document() -> Document {
123        // Cropped from https://github.com/J-F-Liu/lopdf/blob/bcb9244f4c862ca90dea3505339fb67185608175/src/creator.rs#L133-L191
124        let mut doc = Document::new();
125
126        doc.trailer.set(
127            "ID",
128            Object::Array(vec![
129                Object::string_literal(b"ABC"),
130                Object::string_literal(b"DEF"),
131            ]),
132        );
133
134        doc
135    }
136
137    #[test]
138    fn test_permissions() {
139        let mut doc = create_test_document();
140        assert_eq!(doc.permissions(), Permissions::default());
141
142        let pma_permissions = Permissions::from_str("pma");
143        doc.set_permissions(pma_permissions).unwrap();
144
145        let mut buffer = Vec::new();
146        doc.save_to(&mut buffer).unwrap();
147
148        let doc = Document::load_mem(&buffer).unwrap();
149        assert_eq!(doc.permissions(), pma_permissions);
150    }
151
152    // Test `ShortFlags` trait
153    #[test]
154    fn test_from_str_1() {
155        let permissions = Permissions::from_str("pmc");
156        let expected = Permissions::PRINTABLE | Permissions::MODIFIABLE | Permissions::COPYABLE;
157        assert_eq!(permissions, expected);
158    }
159
160    #[test]
161    fn test_from_str_2() {
162        let permissions = Permissions::from_str("*");
163        let expected = Permissions::all();
164        assert_eq!(permissions, expected);
165    }
166
167    #[test]
168    fn test_apply_modification() {
169        let mut permissions = Permissions::empty();
170        permissions.apply_modification("+p");
171        assert_eq!(permissions, Permissions::PRINTABLE);
172        permissions.apply_modification("-p");
173        assert_eq!(permissions, Permissions::empty());
174        permissions.apply_modification("=m");
175        assert_eq!(permissions, Permissions::MODIFIABLE);
176    }
177}