feat: follow upstream twtxt post rules: reject control characters, allow U+2028, make the length cap configurable
This commit is contained in:
parent
dd6865782a
commit
801c66d3b0
12 changed files with 63 additions and 58 deletions
|
|
@ -228,7 +228,7 @@ impl Feed {
|
|||
})
|
||||
}
|
||||
|
||||
/// Appends a record and its signature line, post rule 5's append-only
|
||||
/// Appends a record and its signature line, post rule 4's append-only
|
||||
/// half. The caller has validated the record and checked uniqueness.
|
||||
pub fn append(&mut self, record: Record, mail: Option<MailLine>) {
|
||||
let mail_item = mail.map(Item::Mail);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@ pub mod verify;
|
|||
|
||||
pub use envelope::{open, Opened};
|
||||
pub use feed::{Entry, Feed, MailLine, Record};
|
||||
pub use rules::{validate_text, validate_timestamp, RuleError, MAX_BYTES, MAX_CODEPOINTS};
|
||||
pub use rules::{validate_text, validate_timestamp, RuleError};
|
||||
pub use time::{parse as parse_timestamp, render as render_timestamp};
|
||||
pub use verify::{reply_to_body, repost_body, signed_bytes, verify, Report, State};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
//! Post rules 1, 3 and 4 from PLAN.md, enforced at receipt by the daemon and
|
||||
//! Post rules 1 and 3 from PLAN.md, enforced at receipt by the daemon and
|
||||
//! usable by any author-side tool.
|
||||
|
||||
pub const MAX_CODEPOINTS: usize = 160;
|
||||
pub const MAX_BYTES: usize = 640;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RuleError {
|
||||
TooManyCodepoints,
|
||||
TooManyBytes,
|
||||
ForbiddenChar,
|
||||
BadTimestamp,
|
||||
DuplicateTimestamp,
|
||||
|
|
@ -16,9 +11,7 @@ pub enum RuleError {
|
|||
impl std::fmt::Display for RuleError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let msg = match self {
|
||||
RuleError::TooManyCodepoints => "more than 160 Unicode code points",
|
||||
RuleError::TooManyBytes => "more than 640 bytes",
|
||||
RuleError::ForbiddenChar => "contains TAB, LF, CR or U+2028",
|
||||
RuleError::ForbiddenChar => "contains a control character",
|
||||
RuleError::BadTimestamp => "not an RFC 3339 UTC timestamp at second precision",
|
||||
RuleError::DuplicateTimestamp => "timestamp already exists in this feed",
|
||||
};
|
||||
|
|
@ -28,18 +21,13 @@ impl std::fmt::Display for RuleError {
|
|||
|
||||
impl std::error::Error for RuleError {}
|
||||
|
||||
/// Rules 3 and 4: at most 160 code points and 640 bytes, and no TAB, LF, CR
|
||||
/// or U+2028 — any of those would break the one-record-per-line invariant.
|
||||
/// The byte cap is mathematically implied by the codepoint cap over UTF-8 but
|
||||
/// is kept as an explicit guard.
|
||||
/// Rule 3: no control characters, as twtxt requires. TAB, LF and CR are among
|
||||
/// them, so a record can never break the one-record-per-line invariant. Length
|
||||
/// is an instance policy, so the daemon caps it, not this rule. U+2028
|
||||
/// is not a control character and stays allowed: it is the twtxt Multiline
|
||||
/// extension's line break.
|
||||
pub fn validate_text(text: &str) -> Result<(), RuleError> {
|
||||
if text.chars().count() > MAX_CODEPOINTS {
|
||||
return Err(RuleError::TooManyCodepoints);
|
||||
}
|
||||
if text.len() > MAX_BYTES {
|
||||
return Err(RuleError::TooManyBytes);
|
||||
}
|
||||
if text.contains(['\t', '\n', '\r', '\u{2028}']) {
|
||||
if text.chars().any(char::is_control) {
|
||||
return Err(RuleError::ForbiddenChar);
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -64,24 +52,14 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_over_length_text() {
|
||||
let chars: String = std::iter::repeat('a').take(161).collect();
|
||||
assert_eq!(validate_text(&chars), Err(RuleError::TooManyCodepoints));
|
||||
|
||||
// 160 four-byte code points sit exactly on both caps.
|
||||
let four_byte: String = "🙂".repeat(160);
|
||||
assert_eq!(validate_text(&four_byte), Ok(()));
|
||||
let over: String = "🙂".repeat(161);
|
||||
assert_eq!(
|
||||
validate_text(&over),
|
||||
Err(RuleError::TooManyCodepoints),
|
||||
"codepoints are checked before bytes"
|
||||
);
|
||||
fn accepts_long_and_multiline_text() {
|
||||
assert_eq!(validate_text(&"🙂".repeat(1000)), Ok(()));
|
||||
assert_eq!(validate_text("first\u{2028}second"), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_record_breaking_characters() {
|
||||
for bad in ["a\tb", "a\nb", "a\rb", "a\u{2028}b"] {
|
||||
fn rejects_control_characters() {
|
||||
for bad in ["a\tb", "a\nb", "a\rb", "a\u{0}b", "a\u{1b}[31mb", "a\u{85}b"] {
|
||||
assert_eq!(validate_text(bad), Err(RuleError::ForbiddenChar));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue