2021-11-12 09:19:01 +01:00
|
|
|
// Copyright: Ankitects Pty Ltd and contributors
|
|
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
|
|
|
|
// copied from https://github.com/projectfluent/fluent-rs/pull/241
|
|
|
|
|
Rework syncing code, and replace local sync server (#2329)
This PR replaces the existing Python-driven sync server with a new one in Rust.
The new server supports both collection and media syncing, and is compatible
with both the new protocol mentioned below, and older clients. A setting has
been added to the preferences screen to point Anki to a local server, and a
similar setting is likely to come to AnkiMobile soon.
Documentation is available here: <https://docs.ankiweb.net/sync-server.html>
In addition to the new server and refactoring, this PR also makes changes to the
sync protocol. The existing sync protocol places payloads and metadata inside a
multipart POST body, which causes a few headaches:
- Legacy clients build the request in a non-deterministic order, meaning the
entire request needs to be scanned to extract the metadata.
- Reqwest's multipart API directly writes the multipart body, without exposing
the resulting stream to us, making it harder to track the progress of the
transfer. We've been relying on a patched version of reqwest for timeouts,
which is a pain to keep up to date.
To address these issues, the metadata is now sent in a HTTP header, with the
data payload sent directly in the body. Instead of the slower gzip, we now
use zstd. The old timeout handling code has been replaced with a new implementation
that wraps the request and response body streams to track progress, allowing us
to drop the git dependencies for reqwest, hyper-timeout and tokio-io-timeout.
The main other change to the protocol is that one-way syncs no longer need to
downgrade the collection to schema 11 prior to sending.
2023-01-18 03:43:46 +01:00
|
|
|
use std::fmt::{
|
|
|
|
Error, Write, {self},
|
|
|
|
};
|
2021-11-12 09:19:01 +01:00
|
|
|
|
2022-12-16 12:40:27 +01:00
|
|
|
use fluent_syntax::{ast::*, parser::Slice};
|
|
|
|
|
2021-11-12 09:19:01 +01:00
|
|
|
pub fn serialize<'s, S: Slice<'s>>(resource: &Resource<S>) -> String {
|
|
|
|
serialize_with_options(resource, Options::default())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn serialize_with_options<'s, S: Slice<'s>>(
|
|
|
|
resource: &Resource<S>,
|
|
|
|
options: Options,
|
|
|
|
) -> String {
|
|
|
|
let mut ser = Serializer::new(options);
|
|
|
|
|
|
|
|
ser.serialize_resource(resource)
|
|
|
|
.expect("Writing to an in-memory buffer never fails");
|
|
|
|
|
|
|
|
ser.into_serialized_text()
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Serializer {
|
|
|
|
writer: TextWriter,
|
|
|
|
options: Options,
|
|
|
|
state: State,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Serializer {
|
|
|
|
pub fn new(options: Options) -> Self {
|
|
|
|
Serializer {
|
|
|
|
writer: TextWriter::default(),
|
|
|
|
options,
|
|
|
|
state: State::default(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn serialize_resource<'s, S: Slice<'s>>(&mut self, res: &Resource<S>) -> Result<(), Error> {
|
|
|
|
for entry in &res.body {
|
|
|
|
match entry {
|
|
|
|
Entry::Message(msg) => self.serialize_message(msg)?,
|
|
|
|
Entry::Term(term) => self.serialize_term(term)?,
|
|
|
|
Entry::Comment(comment) => self.serialize_free_comment(comment, "#")?,
|
|
|
|
Entry::GroupComment(comment) => self.serialize_free_comment(comment, "##")?,
|
|
|
|
Entry::ResourceComment(comment) => self.serialize_free_comment(comment, "###")?,
|
|
|
|
Entry::Junk { content } if self.options.with_junk => {
|
|
|
|
self.serialize_junk(content.as_ref())?
|
|
|
|
}
|
|
|
|
Entry::Junk { .. } => continue,
|
|
|
|
}
|
|
|
|
|
|
|
|
self.state.has_entries = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn into_serialized_text(self) -> String {
|
|
|
|
self.writer.buffer
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_junk(&mut self, junk: &str) -> Result<(), Error> {
|
|
|
|
self.writer.write_literal(junk)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_free_comment<'s, S: Slice<'s>>(
|
|
|
|
&mut self,
|
|
|
|
comment: &Comment<S>,
|
|
|
|
prefix: &str,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
if self.state.has_entries {
|
|
|
|
self.writer.newline();
|
|
|
|
}
|
|
|
|
self.serialize_comment(comment, prefix)?;
|
|
|
|
self.writer.newline();
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_comment<'s, S: Slice<'s>>(
|
|
|
|
&mut self,
|
|
|
|
comment: &Comment<S>,
|
|
|
|
prefix: &str,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
for line in &comment.content {
|
|
|
|
self.writer.write_literal(prefix)?;
|
|
|
|
|
|
|
|
if !line.as_ref().trim().is_empty() {
|
|
|
|
self.writer.write_literal(" ")?;
|
|
|
|
self.writer.write_literal(line.as_ref())?;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.writer.newline();
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_message<'s, S: Slice<'s>>(&mut self, msg: &Message<S>) -> Result<(), Error> {
|
|
|
|
if let Some(comment) = msg.comment.as_ref() {
|
|
|
|
self.serialize_comment(comment, "#")?;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.writer.write_literal(msg.id.name.as_ref())?;
|
|
|
|
self.writer.write_literal(" =")?;
|
|
|
|
|
|
|
|
if let Some(value) = msg.value.as_ref() {
|
|
|
|
self.serialize_pattern(value)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.serialize_attributes(&msg.attributes)?;
|
|
|
|
|
|
|
|
self.writer.newline();
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_term<'s, S: Slice<'s>>(&mut self, term: &Term<S>) -> Result<(), Error> {
|
|
|
|
if let Some(comment) = term.comment.as_ref() {
|
|
|
|
self.serialize_comment(comment, "#")?;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.writer.write_literal("-")?;
|
|
|
|
self.writer.write_literal(term.id.name.as_ref())?;
|
|
|
|
self.writer.write_literal(" =")?;
|
|
|
|
self.serialize_pattern(&term.value)?;
|
|
|
|
|
|
|
|
self.serialize_attributes(&term.attributes)?;
|
|
|
|
|
|
|
|
self.writer.newline();
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_pattern<'s, S: Slice<'s>>(&mut self, pattern: &Pattern<S>) -> Result<(), Error> {
|
|
|
|
let start_on_newline = pattern.elements.iter().any(|elem| match elem {
|
|
|
|
PatternElement::TextElement { value } => value.as_ref().contains('\n'),
|
|
|
|
PatternElement::Placeable { expression } => is_select_expr(expression),
|
|
|
|
});
|
|
|
|
|
|
|
|
if start_on_newline {
|
|
|
|
self.writer.newline();
|
|
|
|
self.writer.indent();
|
|
|
|
} else {
|
|
|
|
self.writer.write_literal(" ")?;
|
|
|
|
}
|
|
|
|
|
|
|
|
for element in &pattern.elements {
|
|
|
|
self.serialize_element(element)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
if start_on_newline {
|
|
|
|
self.writer.dedent();
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_attributes<'s, S: Slice<'s>>(
|
|
|
|
&mut self,
|
|
|
|
attrs: &[Attribute<S>],
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
if attrs.is_empty() {
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
self.writer.indent();
|
|
|
|
|
|
|
|
for attr in attrs {
|
|
|
|
self.writer.newline();
|
|
|
|
self.serialize_attribute(attr)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.writer.dedent();
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_attribute<'s, S: Slice<'s>>(&mut self, attr: &Attribute<S>) -> Result<(), Error> {
|
|
|
|
self.writer.write_literal(".")?;
|
|
|
|
self.writer.write_literal(attr.id.name.as_ref())?;
|
|
|
|
self.writer.write_literal(" =")?;
|
|
|
|
|
|
|
|
self.serialize_pattern(&attr.value)?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_element<'s, S: Slice<'s>>(
|
|
|
|
&mut self,
|
|
|
|
elem: &PatternElement<S>,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
match elem {
|
|
|
|
PatternElement::TextElement { value } => self.writer.write_literal(value.as_ref()),
|
|
|
|
PatternElement::Placeable { expression } => match expression {
|
|
|
|
Expression::Inline(InlineExpression::Placeable { expression }) => {
|
|
|
|
// A placeable inside a placeable is a special case because we
|
|
|
|
// don't want the braces to look silly (e.g. "{ { Foo() } }").
|
|
|
|
self.writer.write_literal("{{ ")?;
|
|
|
|
self.serialize_expression(expression)?;
|
|
|
|
self.writer.write_literal(" }}")?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
Expression::Select { .. } => {
|
|
|
|
// select adds its own newline and indent, emit the brace
|
|
|
|
// *without* a space so we don't get 5 spaces instead of 4
|
|
|
|
self.writer.write_literal("{ ")?;
|
|
|
|
self.serialize_expression(expression)?;
|
|
|
|
self.writer.write_literal("}")?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
Expression::Inline(_) => {
|
|
|
|
self.writer.write_literal("{ ")?;
|
|
|
|
self.serialize_expression(expression)?;
|
|
|
|
self.writer.write_literal(" }")?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_expression<'s, S: Slice<'s>>(
|
|
|
|
&mut self,
|
|
|
|
expr: &Expression<S>,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
match expr {
|
|
|
|
Expression::Inline(inline) => self.serialize_inline_expression(inline),
|
|
|
|
Expression::Select { selector, variants } => {
|
|
|
|
self.serialize_select_expression(selector, variants)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_inline_expression<'s, S: Slice<'s>>(
|
|
|
|
&mut self,
|
|
|
|
expr: &InlineExpression<S>,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
match expr {
|
|
|
|
InlineExpression::StringLiteral { value } => {
|
|
|
|
self.writer.write_literal("\"")?;
|
|
|
|
self.writer.write_literal(value.as_ref())?;
|
|
|
|
self.writer.write_literal("\"")?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
InlineExpression::NumberLiteral { value } => self.writer.write_literal(value.as_ref()),
|
|
|
|
InlineExpression::VariableReference {
|
|
|
|
id: Identifier { name: value },
|
|
|
|
} => {
|
|
|
|
self.writer.write_literal("$")?;
|
|
|
|
self.writer.write_literal(value.as_ref())?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
InlineExpression::FunctionReference { id, arguments } => {
|
|
|
|
self.writer.write_literal(id.name.as_ref())?;
|
|
|
|
self.serialize_call_arguments(arguments)?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
InlineExpression::MessageReference { id, attribute } => {
|
|
|
|
self.writer.write_literal(id.name.as_ref())?;
|
|
|
|
|
|
|
|
if let Some(attr) = attribute.as_ref() {
|
|
|
|
self.writer.write_literal(".")?;
|
|
|
|
self.writer.write_literal(attr.name.as_ref())?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
InlineExpression::TermReference {
|
|
|
|
id,
|
|
|
|
attribute,
|
|
|
|
arguments,
|
|
|
|
} => {
|
|
|
|
self.writer.write_literal("-")?;
|
|
|
|
self.writer.write_literal(id.name.as_ref())?;
|
|
|
|
|
|
|
|
if let Some(attr) = attribute.as_ref() {
|
|
|
|
self.writer.write_literal(".")?;
|
|
|
|
self.writer.write_literal(attr.name.as_ref())?;
|
|
|
|
}
|
|
|
|
if let Some(args) = arguments.as_ref() {
|
|
|
|
self.serialize_call_arguments(args)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
InlineExpression::Placeable { expression } => {
|
|
|
|
self.writer.write_literal("{")?;
|
|
|
|
self.serialize_expression(expression)?;
|
|
|
|
self.writer.write_literal("}")?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_select_expression<'s, S: Slice<'s>>(
|
|
|
|
&mut self,
|
|
|
|
selector: &InlineExpression<S>,
|
|
|
|
variants: &[Variant<S>],
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
self.serialize_inline_expression(selector)?;
|
|
|
|
self.writer.write_literal(" ->")?;
|
|
|
|
|
|
|
|
self.writer.newline();
|
|
|
|
self.writer.indent();
|
|
|
|
|
|
|
|
for variant in variants {
|
|
|
|
self.serialize_variant(variant)?;
|
|
|
|
self.writer.newline();
|
|
|
|
}
|
|
|
|
|
|
|
|
self.writer.dedent();
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_variant<'s, S: Slice<'s>>(&mut self, variant: &Variant<S>) -> Result<(), Error> {
|
|
|
|
if variant.default {
|
|
|
|
self.writer.write_char_into_indent('*');
|
|
|
|
}
|
|
|
|
|
|
|
|
self.writer.write_literal("[")?;
|
|
|
|
self.serialize_variant_key(&variant.key)?;
|
|
|
|
self.writer.write_literal("]")?;
|
|
|
|
self.serialize_pattern(&variant.value)?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_variant_key<'s, S: Slice<'s>>(
|
|
|
|
&mut self,
|
|
|
|
key: &VariantKey<S>,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
match key {
|
|
|
|
VariantKey::NumberLiteral { value } | VariantKey::Identifier { name: value } => {
|
|
|
|
self.writer.write_literal(value.as_ref())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_call_arguments<'s, S: Slice<'s>>(
|
|
|
|
&mut self,
|
|
|
|
args: &CallArguments<S>,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
let mut argument_written = false;
|
|
|
|
|
|
|
|
self.writer.write_literal("(")?;
|
|
|
|
|
|
|
|
for positional in &args.positional {
|
|
|
|
if argument_written {
|
|
|
|
self.writer.write_literal(", ")?;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.serialize_inline_expression(positional)?;
|
|
|
|
argument_written = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
for named in &args.named {
|
|
|
|
if argument_written {
|
|
|
|
self.writer.write_literal(", ")?;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.writer.write_literal(named.name.name.as_ref())?;
|
|
|
|
self.writer.write_literal(": ")?;
|
|
|
|
self.serialize_inline_expression(&named.value)?;
|
|
|
|
argument_written = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.writer.write_literal(")")?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_select_expr<'s, S: Slice<'s>>(expr: &Expression<S>) -> bool {
|
|
|
|
match expr {
|
|
|
|
Expression::Select { .. } => true,
|
|
|
|
Expression::Inline(InlineExpression::Placeable { expression }) => {
|
2022-09-24 03:12:58 +02:00
|
|
|
is_select_expr(expression)
|
2021-11-12 09:19:01 +01:00
|
|
|
}
|
|
|
|
Expression::Inline(_) => false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-24 03:12:58 +02:00
|
|
|
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
|
2021-11-12 09:19:01 +01:00
|
|
|
pub struct Options {
|
|
|
|
pub with_junk: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Default, PartialEq)]
|
|
|
|
struct State {
|
|
|
|
has_entries: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
|
|
struct TextWriter {
|
|
|
|
buffer: String,
|
|
|
|
indent_level: usize,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TextWriter {
|
|
|
|
fn indent(&mut self) {
|
|
|
|
self.indent_level += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
fn dedent(&mut self) {
|
|
|
|
self.indent_level = self
|
|
|
|
.indent_level
|
|
|
|
.checked_sub(1)
|
|
|
|
.expect("Dedenting without a corresponding indent");
|
|
|
|
}
|
|
|
|
|
|
|
|
fn write_indent(&mut self) {
|
|
|
|
for _ in 0..self.indent_level {
|
|
|
|
self.buffer.push_str(" ");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn newline(&mut self) {
|
|
|
|
self.buffer.push('\n');
|
|
|
|
}
|
|
|
|
|
|
|
|
fn write_literal(&mut self, mut item: &str) -> fmt::Result {
|
|
|
|
if self.buffer.ends_with('\n') {
|
|
|
|
// we've just added a newline, make sure it's properly indented
|
|
|
|
self.write_indent();
|
|
|
|
|
|
|
|
// we've just added indentation, so we don't care about leading
|
|
|
|
// spaces
|
|
|
|
item = item.trim_start_matches(' ');
|
|
|
|
}
|
|
|
|
|
|
|
|
write!(self.buffer, "{}", item)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn write_char_into_indent(&mut self, ch: char) {
|
|
|
|
if self.buffer.ends_with('\n') {
|
|
|
|
self.write_indent();
|
|
|
|
}
|
|
|
|
self.buffer.pop();
|
|
|
|
self.buffer.push(ch);
|
|
|
|
}
|
|
|
|
}
|