Skip to content

pimalaya/io-webdav

Repository files navigation

I/O WebDAV Documentation Matrix Mastodon

WebDAV client library, written in Rust.

This library is composed of 3 feature-gated layers:

  • Low-level I/O-free coroutines: these no_std-compatible state machines contain the whole WebDAV logic and can be used anywhere
  • Mid-level light client: a standard, blocking WebDAV client using a Stream: Read + Write
  • High-level full client: light client + TCP connections and TLS negotiations handled for you

Table of contents

Features

  • I/O-free coroutines: no_std state machines; no sockets, no async runtime, no std required, drive against any blocking, async, or fuzz harness.
  • Light standard, blocking client (requires client feature)
  • Full standard, blocking client with TLS support:
    • Rustls with ring crypto (requires rustls-ring feature)
    • Rustls with aws crypto (requires rustls-aws feature)
    • Native TLS (requires native-tls feature)
  • CalDAV calendars and items
  • CardDAV addressbooks and cards
  • HTTP Auth mechanisms: BASIC, BEARER

Tip

I/O WebDAV is written in Rust and uses cargo features to gate backend support. The default feature set is declared in Cargo.toml or on docs.rs.

RFC coverage

Module What it covers
4918 WebDAV core: PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, DELETE, GET, PUT, OPTIONS, multistatus parsing
4791 CalDAV: calendar collections and calendar object resources (items)
5397 WebDAV current principal: current-user-principal discovery
6352 CardDAV: addressbook collections and address object resources (cards)
6764 Service discovery: .well-known/caldav and .well-known/carddav bootstrap

Usage

I/O WebDAV can be consumed three ways, depending on how much of the I/O stack you want to own. Each mode is gated by cargo features.

Whichever mode you pick, every standard-shape coroutine implements the WebdavCoroutine trait with two associated types: Yield (intermediate progress) and Return (terminal value, by convention Result<Output, Error>). Its resume(arg: Option<&[u8]>) method returns a WebdavCoroutineState<Yield, Return> with two variants:

  • Yielded(Yield): intermediate yield. Most coroutines pick the standard WebdavYield with WantsRead / WantsWrite(Vec<u8>). Pass Some(&[]) after WantsRead to signal EOF.
  • Complete(Return): terminal yield, carrying Ok(Output) on success or Err(Error) on failure.

The redirect-capable discovery coroutines (CurrentUserPrincipal, CalendarHomeSet, AddressbookHomeSet, FollowRedirects) declare their own WebdavRedirectYield which extends the standard variants with WantsRedirect { url, keep_alive, same_origin }: the server responded with a 3xx and the caller chooses whether to open a new connection to url and retry, or surface the redirect as an error.

Coroutines

No features required: works in #![no_std], no sockets, no async runtime. You own the loop and the bytes; the library only produces request bytes and consumes server responses.

Discover the current user principal against a blocking rustls socket:

use std::{io::{Read, Write}, net::TcpStream, sync::Arc};

use io_webdav::{
    coroutine::{WebdavCoroutine, WebdavCoroutineState},
    rfc4918::{WebdavAuth, coroutine::WebdavRedirectYield},
    rfc5397::current_user_principal::CurrentUserPrincipal,
};
use rustls::{ClientConfig, ClientConnection, StreamOwned};
use rustls_platform_verifier::ConfigVerifierExt;
use io_http::rfc7617::basic::HttpAuthBasic;
use url::Url;

let base_url = Url::parse("https://dav.example.org/").unwrap();
let auth = WebdavAuth::Basic(HttpAuthBasic::new("alice", "secret"));

let config = ClientConfig::with_platform_verifier().unwrap();
let server_name = base_url.host_str().unwrap().to_string().try_into().unwrap();
let conn = ClientConnection::new(Arc::new(config), server_name).unwrap();
let tcp = TcpStream::connect((base_url.host_str().unwrap(), 443)).unwrap();
let mut stream = StreamOwned::new(conn, tcp);

let mut coroutine = CurrentUserPrincipal::new(&base_url, &auth, "io-webdav");
let mut arg: Option<&[u8]> = None;
let mut buf = [0u8; 8192];
let mut read_buf = Vec::<u8>::new();

let principal = loop {
    match coroutine.resume(arg.take()) {
        WebdavCoroutineState::Complete(Ok(principal)) => break principal,
        WebdavCoroutineState::Complete(Err(err)) => panic!("{err}"),
        WebdavCoroutineState::Yielded(WebdavRedirectYield::WantsRead) => {
            let n = stream.read(&mut buf).unwrap();
            read_buf.clear();
            read_buf.extend_from_slice(&buf[..n]);
            arg = Some(&read_buf);
        }
        WebdavCoroutineState::Yielded(WebdavRedirectYield::WantsWrite(bytes)) => {
            stream.write_all(&bytes).unwrap();
        }
        WebdavCoroutineState::Yielded(WebdavRedirectYield::WantsRedirect { url, .. }) => {
            todo!("reconnect to {url}");
        }
    }
};

println!("Principal: {principal:?}");

Light client

Enable the client feature. WebdavClientStd::new(stream, auth, base_url) wraps any blocking Read + Write and exposes one method per WebDAV operation, plus the cached discovery flow (current_user_principalcalendar_home_set / addressbook_home_set). You still open the TCP socket and run TLS yourself, and hand over a ready-to-talk stream; the client takes it from there.

[dependencies]
io-webdav = { version = "0.0.1", default-features = false, features = ["client"] }
use std::{net::TcpStream, sync::Arc};

use io_webdav::{client::WebdavClientStd, rfc4918::WebdavAuth};
use rustls::{ClientConfig, ClientConnection, StreamOwned};
use rustls_platform_verifier::ConfigVerifierExt;
use io_http::rfc7617::basic::HttpAuthBasic;
use url::Url;

let base_url = Url::parse("https://dav.example.org/").unwrap();
let auth = WebdavAuth::Basic(HttpAuthBasic::new("alice", "secret"));

let config = ClientConfig::with_platform_verifier().unwrap();
let server_name = base_url.host_str().unwrap().to_string().try_into().unwrap();
let conn = ClientConnection::new(Arc::new(config), server_name).unwrap();
let tcp = TcpStream::connect((base_url.host_str().unwrap(), 443)).unwrap();
let stream = StreamOwned::new(conn, tcp);

let mut client = WebdavClientStd::new(stream, auth, base_url);
client.calendar_home_set().unwrap();
for calendar in client.list_calendars().unwrap() {
    println!("{}: {:?}", calendar.id, calendar.display_name);
}

Full client

Enable one of the TLS feature flags: rustls-ring (default), rustls-aws, or native-tls. WebdavClientStd::connect(url, tls, auth) opens http:// / https:// URLs via pimalaya/stream.

[dependencies]
io-webdav = "0.0.1" # rustls-ring is enabled by default
use io_webdav::{client::WebdavClientStd, rfc4918::WebdavAuth};
use pimalaya_stream::tls::Tls;
use io_http::rfc7617::basic::HttpAuthBasic;
use url::Url;

let base_url = Url::parse("https://dav.example.org/").unwrap();
let auth = WebdavAuth::Basic(HttpAuthBasic::new("alice", "secret"));
let tls = Tls::default();

let mut client = WebdavClientStd::connect(&base_url, &tls, auth).unwrap();
client.calendar_home_set().unwrap();
for calendar in client.list_calendars().unwrap() {
    println!("{}: {:?}", calendar.id, calendar.display_name);
}

When discovery surfaces a different authority than where you first connected (an RFC 6764 .well-known redirect, a cross-origin home-set), use WebdavClientStd::set_stream to swap in a new transport.

AI disclosure

This project is developed with AI assistance. This section documents how, so users and downstream packagers can make informed decisions.

  • Tools: Claude Code (Anthropic), Opus 4.8, invoked locally with a persistent project-scoped memory and a small set of repo-specific rules.

  • Used for: Refactors, mechanical multi-file edits, boilerplate (feature gates, error enums, derive macros, trait impls), test scaffolding, doc polish, exploratory design conversations.

  • Not used for: Engineering, critical code, git manipulation (commit, merge, rebase…), real-world tests.

  • Verification: Every AI-assisted change is read, compiled, tested, and formatted before commit (nix develop --command cargo check / cargo test / cargo fmt). Behavioural correctness is verified against the relevant RFC or upstream spec, not assumed from the model output. Tests are never adjusted to fit AI-generated code; the code is adjusted to fit correct behaviour.

  • Limitations: AI models occasionally produce code that compiles and passes tests but is subtly wrong: off-by-one errors, missed edge cases, plausible but nonexistent APIs, stale RFC references. The verification workflow catches most of this; it does not catch all of it. Bug reports are welcome and taken seriously.

  • Last reviewed: 10/06/2026

License

This project is licensed under either of:

at your option.

Social

Sponsoring

nlnet

Special thanks to the NLnet foundation and the European Commission that have been financially supporting the project for years:

If you appreciate the project, feel free to donate using one of the following providers:

GitHub Ko-fi Buy Me a Coffee Liberapay thanks.dev PayPal