librecast 1.0.1

Rust bindings for the librecast library
Documentation
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
// Copyright (c) 2025 Gavin Henry <ghenry@sentrypeer.org>

//! Rust bindings for the librecast library.
//!
//! This crate provides a safe, ergonomic Rust interface to the librecast C library
//! via the lower-level `librecast-sys` crate.

use std::ffi::CString;
use std::marker::PhantomData;

// Re-export librecast-sys for advanced users
pub use librecast_sys;

// Error handling
#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Librecast initialization failed: {0}")]
    InitializationError(String),

    #[error("Invalid configuration: {0}")]
    ConfigurationError(String),

    #[error("Operation failed: {0}")]
    OperationError(String),

    #[error("Null pointer encountered")]
    NullPointerError,

    #[error("String conversion error")]
    StringConversionError,
}

pub type Result<T> = std::result::Result<T, Error>;

// Librecast context struct - a safe wrapper around lc_ctx_t
pub struct Librecast {
    ctx: *mut librecast_sys::lc_ctx_t,
}

impl Librecast {
    /// Create a new librecast context
    pub fn new() -> Result<Self> {
        let ctx = unsafe { librecast_sys::lc_ctx_new() };
        if ctx.is_null() {
            return Err(Error::InitializationError(
                "Failed to create context".to_string(),
            ));
        }
        Ok(Self { ctx })
    }

    /// Get a builder for creating a new channel
    pub fn channel_builder(&self) -> ChannelBuilder {
        ChannelBuilder::new(self)
    }

    // Add other context methods here
}

impl Drop for Librecast {
    fn drop(&mut self) {
        if !self.ctx.is_null() {
            unsafe {
                librecast_sys::lc_ctx_free(self.ctx);
            }
        }
    }
}

// Make Context safe to send between threads if the underlying C library supports it
unsafe impl Send for Librecast {}

// Channel builder - implements the Builder pattern
pub struct ChannelBuilder<'a> {
    context: &'a Librecast,
    name: Option<String>,
    enable_raptorq: bool,  // Add other channel configuration options
    enable_loopback: bool, // Receive our own packets on the same host
}

impl<'a> ChannelBuilder<'a> {
    fn new(context: &'a Librecast) -> Self {
        Self {
            context,
            name: None,
            enable_raptorq: false,
            enable_loopback: false,
        }
    }

    /// Set the channel name
    pub fn name(mut self, name: impl Into<String>) -> Self {
        self.name = Some(name.into());
        self
    }

    /// Enable RaptorQ encoding (RFC6330). Requires liblcrq to be installed for
    /// librecast-sys to link against.
    pub fn enable_raptorq(mut self) -> Self {
        self.enable_raptorq = true;
        self
    }

    /// Enable loopback mode (receive our own packets)
    pub fn enable_loopback(mut self) -> Self {
        self.enable_loopback = true;
        self
    }

    /// Build the channel
    pub fn build(self) -> Result<Channel<'a>> {
        // Validate required fields
        let name = self
            .name
            .ok_or_else(|| Error::ConfigurationError("Channel name is required".to_string()))?;

        let name_c = CString::new(name).map_err(|_| Error::StringConversionError)?;

        let channel = unsafe { librecast_sys::lc_channel_new(self.context.ctx, name_c.into_raw()) };

        if channel.is_null() {
            return Err(Error::InitializationError(
                "Failed to create a channel".to_string(),
            ));
        }

        // Create a Librecast Socket (IPv6)
        let socket = unsafe { librecast_sys::lc_socket_new(self.context.ctx) };

        if socket.is_null() {
            unsafe {
                librecast_sys::lc_channel_free(channel);
            }
            return Err(Error::InitializationError(
                "Failed to create a socket".to_string(),
            ));
        }

        // Bind the channel to the Socket
        let bind_rc = unsafe { librecast_sys::lc_channel_bind(socket, channel) };

        if bind_rc != 0 {
            unsafe {
                librecast_sys::lc_channel_free(channel);
            }
            return Err(Error::OperationError(
                "Failed to bind a channel to a socket".to_string(),
            ));
        }

        if self.enable_raptorq {
            let result = unsafe {
                librecast_sys::lc_channel_coding_set(
                    channel,
                    (librecast_sys::lc_coding_t_LC_CODE_FEC_RQ
                        | librecast_sys::lc_coding_t_LC_CODE_FEC_OTI)
                        .try_into()
                        .unwrap(),
                )
            };

            // Check if the codec was set successfully as the result of
            // setting the codec should be 40 (LC_CODE_FEC_RQ | LC_CODE_FEC_OTI)
            // TODO: There is a third option that can be used, so make this easier
            // to set. See man lc_channel_coding_set(3)
            if result != 40 {
                // Clean up on error
                unsafe {
                    librecast_sys::lc_channel_free(channel);
                }
                return Err(Error::ConfigurationError(
                    "Failed to set channel codec".to_string(),
                ));
            }
        }

        if self.enable_loopback {
            // Enable loopback mode
            let result = unsafe { librecast_sys::lc_socket_loop(socket, 1) };
            if result != 0 {
                // Clean up on error
                unsafe {
                    librecast_sys::lc_channel_free(channel);
                }
                return Err(Error::ConfigurationError(
                    "Failed to enable loopback mode".to_string(),
                ));
            }
        }

        Ok(Channel {
            channel,
            _context: PhantomData, // Ensure channel doesn't outlive the context
        })
    }
}

// Channel struct - safe wrapper around lc_channel_t
pub struct Channel<'a> {
    channel: *mut librecast_sys::lc_channel_t,
    _context: PhantomData<&'a Librecast>,
}

impl<'a> Channel<'a> {
    // Implement channel methods

    /// Join this channel
    pub fn join(&self) -> Result<()> {
        let result = unsafe { librecast_sys::lc_channel_join(self.channel) };
        if result != 0 {
            return Err(Error::OperationError(
                "Failed to join a channel".to_string(),
            ));
        }
        Ok(())
    }

    /// Leave this channel
    pub fn leave(&self) -> Result<()> {
        let result = unsafe { librecast_sys::lc_channel_part(self.channel) };
        if result != 0 {
            return Err(Error::OperationError(
                "Failed to leave a channel".to_string(),
            ));
        }
        Ok(())
    }

    /// Rate-limit sending
    /// 104857600 for 100 Mbps
    // TODO: Use human readable values like 100 Mbps, 1 Gbps, etc.
    pub fn rate_limit(&self, limit: u32) -> Result<()> {
        unsafe { librecast_sys::lc_channel_ratelimit(self.channel, limit as usize, 0) };
        Ok(())
    }

    /// Send some multicast data
    pub fn send(&self, data: &[u8]) -> Result<()> {
        if data.is_empty() {
            return Err(Error::OperationError("Data cannot be empty".to_string()));
        }

        let result = unsafe {
            librecast_sys::lc_channel_send(
                self.channel,
                data.as_ptr() as *const libc::c_void,
                data.len(),
                0,
            )
        };

        if result == -1 {
            return Err(Error::OperationError(
                "Failed to send data on a channel".to_string(),
            ));
        }
        Ok(())
    }

    /// Receive some multicast data
    pub fn receive(&self, buffer: &mut [u8]) -> Result<usize> {
        if buffer.is_empty() {
            return Err(Error::OperationError("Buffer cannot be empty".to_string()));
        }

        let result = unsafe {
            librecast_sys::lc_channel_recv(
                self.channel,
                buffer.as_mut_ptr() as *mut libc::c_void,
                buffer.len(),
                0,
            )
        };

        if result == -1 {
            return Err(Error::OperationError(
                "Failed to receive data on a channel".to_string(),
            ));
        }
        Ok(result as usize)
    }
}

impl<'a> Drop for Channel<'a> {
    fn drop(&mut self) {
        if !self.channel.is_null() {
            unsafe {
                librecast_sys::lc_channel_free(self.channel);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        // Test creating a context
        let librecast = Librecast::new().expect("Failed to create test context");
        assert!(!librecast.ctx.is_null(), "Context should not be null");

        // Test building a channel
        let channel = librecast
            .channel_builder()
            .name("test_channel")
            .enable_loopback() // Enable loopback for testing
            .build()
            .expect("Failed to build a test channel");
        assert!(!channel.channel.is_null(), "Channel should not be null");

        // Test joining the channel
        channel.join().expect("Failed to join a test channel");

        // Rate-limit sending
        channel
            .rate_limit(104857600) // 100 Mbps
            .expect("Failed to set a rate limit for our test channel");

        // Test sending data
        let data = b"Hello, Librecast!";
        channel
            .send(data)
            .expect("Failed to send data on the test channel");

        // Test receiving data
        let mut buffer = vec![0u8; 1024]; // Create a buffer for receiving data
        let received_size = channel
            .receive(&mut buffer)
            .expect("Failed to receive data on the test channel");

        assert!(received_size > 0, "Received size should be greater than 0");

        // Test leaving the channel
        channel.leave().expect("Failed to leave a test channel");
    }
}