blob: 02b3b3315ea4135cc9b67719aacbeee195e2f92c [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::*;
use std::io::Write;
use tokio::sync::mpsc;
fn fixture_source(temp: &tempfile::TempDir, digit: char) -> Source {
Source::for_root(
&digit.to_string().repeat(64),
&RootNamespaces {
ownership: temp.path().join("owners"),
control: temp.path().join("control"),
},
)
.unwrap()
}
fn tail() -> HostLogRequest {
HostLogRequest {
position: HostLogPosition::Tail,
}
}
fn after(cursor: HostLogCursor) -> HostLogRequest {
HostLogRequest {
position: HostLogPosition::After { cursor },
}
}
fn data(page: HostLogPage) -> (HostLogCursor, Vec<u8>, Option<HostLogReset>, bool, bool) {
match page {
HostLogPage::Data {
cursor,
bytes,
reset,
truncated,
has_more,
} => (
cursor,
STANDARD.decode(bytes).unwrap(),
reset,
truncated,
has_more,
),
HostLogPage::Missing => panic!("expected captured file"),
}
}
#[tokio::test]
async fn bounded_binary_tail_and_forward_cursor_preserve_exact_bytes() {
let temp = tempfile::tempdir().unwrap();
let source = fixture_source(&temp, 'a');
assert_eq!(source.read(tail()).await.unwrap(), HostLogPage::Missing);
assert!(
!source.path().parent().unwrap().exists(),
"read must not create a directory"
);
let mut writer = source.open_capture().unwrap();
let (_, empty, reset, truncated, more) = data(source.read(tail()).await.unwrap());
assert!(empty.is_empty());
assert_eq!((reset, truncated, more), (None, false, false));
let initial: Vec<_> = (0..RAW_LIMIT + 17).map(|i| i as u8).collect();
writer.write_all(&initial).unwrap();
let page = source.read(tail()).await.unwrap();
assert!(serde_json::to_vec(&page).unwrap().len() <= 48 * 1024);
let (cursor, bytes, reset, truncated, more) = data(page);
assert_eq!(bytes, initial[17..]);
assert_eq!((reset, truncated, more), (None, true, false));
let later: Vec<_> = (0..RAW_LIMIT + 9).map(|i| (i % 251) as u8).collect();
writer.write_all(&later).unwrap();
let serialized = serde_json::to_string(&after(cursor)).unwrap();
let request = serde_json::from_str(&serialized).unwrap();
let (cursor, bytes, reset, truncated, more) = data(source.read(request).await.unwrap());
assert_eq!(bytes, later[..RAW_LIMIT]);
assert_eq!((reset, truncated, more), (None, false, true));
let (cursor, bytes, reset, truncated, more) = data(source.read(after(cursor)).await.unwrap());
assert_eq!(bytes, later[RAW_LIMIT..]);
assert_eq!((reset, truncated, more), (None, false, false));
// Deliberately split a UTF-8 sequence and include invalid UTF-8. Cursors
// address raw bytes, not independently decoded text chunks.
writer.write_all(&[0xe4, 0xb8]).unwrap();
let (cursor, bytes, ..) = data(source.read(after(cursor)).await.unwrap());
assert_eq!(bytes, [0xe4, 0xb8]);
writer.write_all(&[0xad, 0xff, 0]).unwrap();
let (_, bytes, ..) = data(source.read(after(cursor)).await.unwrap());
assert_eq!(bytes, [0xad, 0xff, 0]);
}
#[tokio::test]
async fn replacement_shrink_and_foreign_or_invalid_requests_are_explicit() {
let temp = tempfile::tempdir().unwrap();
let source = fixture_source(&temp, 'b');
source
.open_capture()
.unwrap()
.write_all(b"original-long")
.unwrap();
let (cursor, ..) = data(source.read(tail()).await.unwrap());
let parked = source.path().with_extension("old");
std::fs::rename(source.path(), &parked).unwrap();
assert_eq!(
source.read(after(cursor.clone())).await.unwrap(),
HostLogPage::Missing
);
source
.open_capture()
.unwrap()
.write_all(b"replacement")
.unwrap();
let (cursor, bytes, reset, _, _) = data(source.read(after(cursor)).await.unwrap());
assert_eq!(bytes, b"replacement");
assert_eq!(reset, Some(HostLogReset::Replaced));
let mut writer = std::fs::OpenOptions::new()
.write(true)
.open(source.path())
.unwrap();
writer.set_len(0).unwrap();
writer.write_all(b"new").unwrap();
let (cursor, bytes, reset, _, _) = data(source.read(after(cursor)).await.unwrap());
assert_eq!(bytes, b"new");
assert_eq!(reset, Some(HostLogReset::Truncated));
let other = fixture_source(&temp, 'c');
assert_eq!(
other.read(after(cursor)).await.unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
for input in ["bad".to_owned(), "a".repeat(513)] {
assert!(input.parse::<HostLogCursor>().is_err());
}
assert!(
serde_json::from_value::<HostLogRequest>(
serde_json::json!({"position":{"kind":"tail"},"path":"/tmp/secret"})
)
.is_err()
);
assert!(
Source::for_root(
"../other",
&RootNamespaces {
ownership: temp.path().into(),
control: temp.path().into()
}
)
.is_err()
);
}
#[tokio::test]
async fn watch_retains_cursor_across_missing_and_settles_idle_cancellation() {
let temp = tempfile::tempdir().unwrap();
let source = fixture_source(&temp, 'd');
source.open_capture().unwrap().write_all(b"before").unwrap();
let (cursor, ..) = data(source.read(tail()).await.unwrap());
let parked = source.path().with_extension("parked");
std::fs::rename(source.path(), &parked).unwrap();
let cancel = CancellationToken::new();
let watching = source.clone();
let cancellation = cancel.clone();
// A bounded delivery lane proves watch does not rely on unbounded buffering.
let (send, mut receive) = mpsc::channel(1);
let task = tokio::spawn(async move {
watching
.watch(after(cursor), cancellation, |page| {
let send = send.clone();
async move { send.send(page).await.map_err(io::Error::other) }
})
.await
});
let first = tokio::time::timeout(Duration::from_secs(3), receive.recv())
.await
.unwrap()
.unwrap();
assert_eq!(first, HostLogPage::Missing);
let mut writer = std::fs::OpenOptions::new()
.append(true)
.open(&parked)
.unwrap();
writer.write_all(b"after").unwrap();
std::fs::rename(&parked, source.path()).unwrap();
let next = tokio::time::timeout(Duration::from_secs(3), receive.recv())
.await
.unwrap()
.unwrap();
let (_, bytes, reset, _, _) = data(next);
assert_eq!(
bytes, b"after",
"missing must not discard the original byte cursor"
);
assert_eq!(reset, None);
cancel.cancel();
tokio::time::timeout(Duration::from_secs(3), task)
.await
.unwrap()
.unwrap()
.unwrap();
assert!(receive.recv().await.is_none());
}
#[tokio::test]
async fn source_does_not_keep_root_ownership_alive() {
let temp = tempfile::tempdir().unwrap();
let namespaces = RootNamespaces {
ownership: temp.path().join("owners"),
control: temp.path().join("control"),
};
let root_path = temp.path().join("root");
let owner = RootOwner::create(&root_path, &namespaces).unwrap();
let source = Source::from_owner(&owner).unwrap();
drop(owner);
let reopened = RootOwner::open(&root_path, &namespaces).unwrap();
source
.open_capture()
.unwrap()
.write_all(b"still readable")
.unwrap();
assert_eq!(
data(source.read(tail()).await.unwrap()).1,
b"still readable"
);
drop(reopened);
}
#[cfg(unix)]
#[tokio::test]
async fn reads_reject_nonprivate_files_links_and_symlink_directories() {
use std::os::unix::fs::{PermissionsExt, symlink};
let temp = tempfile::tempdir().unwrap();
let source = fixture_source(&temp, 'e');
source
.open_capture()
.unwrap()
.write_all(b"private")
.unwrap();
std::fs::set_permissions(source.path(), std::fs::Permissions::from_mode(0o644)).unwrap();
assert!(source.read(tail()).await.is_err());
assert_eq!(
std::fs::metadata(source.path())
.unwrap()
.permissions()
.mode()
& 0o777,
0o644,
"reader must not chmod"
);
std::fs::set_permissions(source.path(), std::fs::Permissions::from_mode(0o600)).unwrap();
let link = source.path().with_extension("link");
std::fs::hard_link(source.path(), &link).unwrap();
assert!(source.read(tail()).await.is_err());
assert!(source.open_capture().is_err());
std::fs::remove_file(&link).unwrap();
std::fs::rename(source.path(), &link).unwrap();
symlink(&link, source.path()).unwrap();
assert!(source.read(tail()).await.is_err());
assert!(source.open_capture().is_err());
std::fs::remove_file(source.path()).unwrap();
std::fs::rename(&link, source.path()).unwrap();
let parent = source.path().parent().unwrap();
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(source.read(tail()).await.is_err());
assert_eq!(
std::fs::metadata(parent).unwrap().permissions().mode() & 0o777,
0o755
);
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)).unwrap();
let moved = parent.with_extension("old");
std::fs::rename(parent, &moved).unwrap();
symlink(&moved, parent).unwrap();
assert!(source.read(tail()).await.is_err());
}