Skip to content

Commit de7c33a

Browse files
authored
1.5.0-rc.3 (#186)
* chore: bumped version * feat: added locks on evps resolves #161 * style: reformatted * chore: added fslock license
1 parent 41fc53d commit de7c33a

File tree

8 files changed

+110
-30
lines changed

8 files changed

+110
-30
lines changed

Cargo.lock

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "evidenceangel"
33
description = "Library and executables to work with EvidenceAngel evidence packages (*.evp)."
4-
version = "1.5.0-rc.2"
4+
version = "1.5.0-rc.3"
55
edition = "2024"
66
license = "GPL-3.0-or-later"
77
authors = [
@@ -70,6 +70,7 @@ colored = { version = "3.0.0", optional = true }
7070
directories = { version = "6.0.0", optional = true }
7171
fluent = { version = "0.16.1", optional = true }
7272
fluent-templates = { version = "0.13.0", optional = true }
73+
fslock = "0.2.1"
7374
getset = "0.1.2"
7475
html-escape = { version = "0.2.13", optional = true }
7576
infer = "0.19.0"

schemas/draft-hopkins-evp-spec.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,9 @@ See an example <uuid>.json file in (#example-test-case).
180180

181181
The "custom" field is used to add custom metadata that has been
182182
specified in the package manifest's "custom_test_case_metadata" field.
183-
All values **MUST** be strings.
183+
If a value is specified in "custom", it **MUST** be present in the
184+
package manifest, but all values in the package manifest do not need to
185+
be present here. All values **MUST** be strings.
184186

185187
#### "evidence" Array Element {#test-case-evidence}
186188

@@ -225,6 +227,24 @@ an additional media file.
225227

226228
# Handling an Evidence Package
227229

230+
## Locking
231+
232+
When loading an evidence package, implemetors **MUST** use a lock file
233+
with the file name ".~" followed by the full name of the package it
234+
protects, for example for a package called "example.evp", the lock file
235+
**MUST** be called ".~example.evp". It **MUST** be located adjacent (in
236+
the same directory as) the evidence package.
237+
238+
The lock file should be considered as locking the package if:
239+
240+
* the lock file is present, and;
241+
* the lock file contains a process ID.
242+
243+
If either of these is not the case, it should be assumed that the there
244+
is no current lock over the package.
245+
246+
## Media Loading
247+
228248
Software implementing the evidence package format **MUST NOT** load
229249
files from the "media" directory into memory until it is needed for
230250
display or for extraction. Implementors **MUST** use streams to load

src/evidenceangel-ui/about.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ impl SimpleComponent for AppAbout {
3838
add_legal_section: ("directories", None, gtk::License::MitX11, None),
3939
add_legal_section: ("fluent", None, gtk::License::MitX11, None),
4040
add_legal_section: ("fluent-templates", None, gtk::License::MitX11, None),
41+
add_legal_section: ("fslock", None, gtk::License::MitX11, None),
4142
add_legal_section: ("getset", None, gtk::License::MitX11, None),
4243
add_legal_section: ("html-escape", None, gtk::License::MitX11, None),
4344
add_legal_section: ("infer", None, gtk::License::MitX11, None),

src/evidenceangel-ui/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ fn main() {
8383
let cli = Args::parse();
8484

8585
let app = RelmApp::new("uk.hpkns.EvidenceAngel");
86-
relm4::main_application().set_flags(ApplicationFlags::HANDLES_OPEN);
86+
relm4::main_application()
87+
.set_flags(ApplicationFlags::HANDLES_OPEN | ApplicationFlags::NON_UNIQUE);
8788
relm4::main_application().connect_open(|_app, _files, _hint| {
8889
// nothing to do, this is handled by clap...
8990
});

src/package.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,30 +95,30 @@ impl fmt::Debug for EvidencePackage {
9595
impl EvidencePackage {
9696
/// Create a new evidence package.
9797
///
98-
/// # Panics
99-
///
100-
/// All the potential panics are checked statically ahead of time, so should never trigger at runtime.
101-
///
10298
/// # Errors
10399
///
104100
/// - [`Error::Io`] if the evp couldn't be written at all.
105101
/// - [`Error::Zip`] if the evp file couldn't be written correctly.
106102
/// - [`Error::ManifestSchemaValidationFailed`] if the manifest is invalid.
103+
#[allow(
104+
clippy::missing_panics_doc,
105+
reason = "panics have been statically validated to never occur"
106+
)]
107107
pub fn new(path: PathBuf, title: String, authors: Vec<Author>) -> Result<Self> {
108108
Self::new_with_description(path, title, None, authors)
109109
}
110110

111111
/// Create a new evidence package with a specified description.
112112
///
113-
/// # Panics
114-
///
115-
/// All the potential panics are checked statically ahead of time, so should never trigger at runtime.
116-
///
117113
/// # Errors
118114
///
119115
/// - [`Error::Io`] if the evp couldn't be written at all.
120116
/// - [`Error::Zip`] if the evp file couldn't be written correctly.
121117
/// - [`Error::ManifestSchemaValidationFailed`] if the manifest is invalid.
118+
#[allow(
119+
clippy::missing_panics_doc,
120+
reason = "panics have been statically validated to never occur"
121+
)]
122122
pub fn new_with_description(
123123
path: PathBuf,
124124
title: String,
@@ -127,7 +127,7 @@ impl EvidencePackage {
127127
) -> Result<Self> {
128128
// Create manifest data.
129129
let mut manifest = Self {
130-
zip: ZipReaderWriter::new(path),
130+
zip: ZipReaderWriter::new(path)?,
131131
media_data: HashMap::new(),
132132
test_case_data: HashMap::new(),
133133

@@ -307,7 +307,7 @@ impl EvidencePackage {
307307
/// - [`Error::TestCaseSchemaValidationFailed`] if one of the test case manifests fails schema validation.
308308
pub fn open(path: PathBuf) -> Result<Self> {
309309
// Open ZIP file
310-
let mut zip_rw = ZipReaderWriter::new(path);
310+
let mut zip_rw = ZipReaderWriter::new(path)?;
311311
let zip = zip_rw.as_reader()?;
312312

313313
// Read manifest
@@ -382,7 +382,7 @@ impl EvidencePackage {
382382
/// Clone fields that will be serialized by serde
383383
fn clone_serde(&self) -> Self {
384384
Self {
385-
zip: ZipReaderWriter::new(PathBuf::new()),
385+
zip: ZipReaderWriter::default(),
386386
media_data: HashMap::new(),
387387
test_case_data: HashMap::new(),
388388

src/result.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ use uuid::Uuid;
44
/// An error raised by `EvidenceAngel`.
55
#[derive(Debug, Error)]
66
pub enum Error {
7+
/// A file lock couldn't be obtained over the package.
8+
#[error("Failed to obtain a lock over the package: {0}")]
9+
Locking(std::io::Error),
10+
11+
/// You are trying to perform an operation without a lock on the package.
12+
#[error("The file you are working with is already open. Please close it and try again.")]
13+
LockNotObtained,
14+
715
/// An I/O error from the system.
816
#[error("I/O Error: {0}")]
917
Io(#[from] std::io::Error),

src/zip_read_writer.rs

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@ use std::{
44
path,
55
};
66

7-
use getset::Setters;
7+
use fslock::LockFile;
88
use zip::{ZipArchive, ZipWriter};
99

1010
/// A convenient type which can read and write to a ZIP file and cleanly switch between the two modes.
1111
///
1212
/// Whilst writing, you can also read the previous file, as it writes to a new temporary file, until
1313
/// [`ZipReaderWriter::conclude_write`] is called.
14-
#[derive(Default, Setters)]
14+
#[derive(Default)]
1515
pub(crate) struct ZipReaderWriter {
1616
/// The path of this zip file, if set
17-
#[getset(set = "pub")]
18-
file: Option<path::PathBuf>,
17+
path: Option<path::PathBuf>,
18+
/// The locking file for this evidence package. If this is [`Some`],
19+
/// you can be assured that the lock is obtained.
20+
lock_file: Option<LockFile>,
1921
/// The reader, if in write mode, of this reader/writer
2022
reader: Option<ZipArchive<BufReader<fs::File>>>,
2123
/// The writer, if in write mode, of this reader/writer
@@ -25,7 +27,7 @@ pub(crate) struct ZipReaderWriter {
2527
impl Clone for ZipReaderWriter {
2628
fn clone(&self) -> Self {
2729
Self {
28-
file: self.file.clone(),
30+
path: self.path.clone(),
2931
..Default::default()
3032
}
3133
}
@@ -41,19 +43,46 @@ impl fmt::Debug for ZipReaderWriter {
4143
"idle"
4244
};
4345
f.debug_struct("ZipReadWriter")
44-
.field("file", &self.file)
46+
.field("file", &self.path)
4547
.field("current_mode", &mode)
46-
.finish()
48+
.finish_non_exhaustive()
4749
}
4850
}
4951

5052
impl ZipReaderWriter {
5153
/// Create a new [`ZipReaderWriter`] instance.
52-
pub fn new(path: path::PathBuf) -> Self {
53-
Self {
54-
file: Some(path),
54+
pub fn new(path: path::PathBuf) -> crate::Result<Self> {
55+
let mut o = Self {
56+
path: Some(path),
5557
..Default::default()
58+
};
59+
o.update_lock_file()?;
60+
Ok(o)
61+
}
62+
63+
/// Update the locking file for this [`ZipReaderWriter`]. This will
64+
/// either obtain it (if a path is set), drop it (if a path isn't
65+
/// set), or will return a [`crate::Error::Locking`] error.
66+
fn update_lock_file(&mut self) -> crate::Result<()> {
67+
if let Some(path) = &self.path {
68+
let mut lock_path = path.clone();
69+
// SAFETY: only a file can be specified here
70+
lock_path.set_file_name(format!(
71+
".~{}",
72+
lock_path.file_name().unwrap().to_str().unwrap()
73+
));
74+
let mut lock_file = LockFile::open(&lock_path).map_err(crate::Error::Locking)?;
75+
if !lock_file
76+
.try_lock_with_pid()
77+
.map_err(crate::Error::Locking)?
78+
{
79+
return Err(crate::Error::LockNotObtained);
80+
}
81+
self.lock_file = Some(lock_file);
82+
} else {
83+
self.lock_file = None;
5684
}
85+
Ok(())
5786
}
5887

5988
/// Get this [`ZipReaderWriter`] instance in read mode.
@@ -66,7 +95,7 @@ impl ZipReaderWriter {
6695
// Open reader
6796
tracing::debug!("Opening reader");
6897
self.reader = Some(ZipArchive::new(BufReader::new(fs::File::open(
69-
self.file
98+
self.path
7099
.as_ref()
71100
.expect("zipreadwriter must not be called upon until file is set."),
72101
)?))?);
@@ -82,11 +111,14 @@ impl ZipReaderWriter {
82111
Option<&mut ZipArchive<BufReader<fs::File>>>,
83112
&mut ZipWriter<BufWriter<fs::File>>,
84113
)> {
114+
if self.lock_file.is_none() {
115+
return Err(crate::Error::LockNotObtained);
116+
}
85117
if self.writer.is_none() {
86118
tracing::debug!("Opening writer");
87119
// Open writer
88120
self.writer = Some(ZipWriter::new(BufWriter::new(fs::File::create(
89-
self.file
121+
self.path
90122
.as_ref()
91123
.map(|p| {
92124
let mut p = p.clone();
@@ -104,6 +136,9 @@ impl ZipReaderWriter {
104136

105137
/// Conclude writing to the ZIP file and reset for reading or writing again.
106138
pub fn conclude_write(&mut self) -> crate::Result<()> {
139+
if self.lock_file.is_none() {
140+
return Err(crate::Error::LockNotObtained);
141+
}
107142
if self.writer.is_some() {
108143
// Close write
109144
tracing::debug!("Closing writer");
@@ -116,7 +151,7 @@ impl ZipReaderWriter {
116151
// Move temp file
117152
tracing::debug!("Moving temp file to overwrite package");
118153
let tmp_path = self
119-
.file
154+
.path
120155
.as_ref()
121156
.map(|p| {
122157
let mut p = p.clone();
@@ -126,7 +161,7 @@ impl ZipReaderWriter {
126161
.expect("zipreadwriter must not be called upon until file is set.");
127162
fs::rename(
128163
tmp_path,
129-
self.file
164+
self.path
130165
.as_ref()
131166
.expect("zipreadwriter must not be called upon until file is set."),
132167
)?;
@@ -136,6 +171,9 @@ impl ZipReaderWriter {
136171

137172
/// Interrupt a write early, concluding the write and removing the temporary file.
138173
pub fn interrupt_write(&mut self) -> crate::Result<()> {
174+
if self.lock_file.is_none() {
175+
return Err(crate::Error::LockNotObtained);
176+
}
139177
if self.writer.is_some() {
140178
// Close write
141179
tracing::debug!("Closing writer");
@@ -148,7 +186,7 @@ impl ZipReaderWriter {
148186
// Delete temp file
149187
tracing::debug!("Moving temp file to overwrite package");
150188
let tmp_path = self
151-
.file
189+
.path
152190
.as_ref()
153191
.map(|p| {
154192
let mut p = p.clone();

0 commit comments

Comments
 (0)