├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── binlog_client.rs └── binlog_reader.rs └── src ├── binlog_client.rs ├── binlog_events.rs ├── binlog_options.rs ├── binlog_reader.rs ├── commands ├── auth_plugin_switch_command.rs ├── authenticate_command.rs ├── command_type.rs ├── dump_binlog_command.rs ├── dump_binlog_gtid_command.rs ├── mod.rs ├── query_command.rs ├── register_slave_command.rs └── ssl_request_command.rs ├── configure.rs ├── connect.rs ├── constants ├── auth_plugin_names.rs ├── capability_flags.rs ├── checksum_type.rs ├── column_type.rs ├── database_provider.rs └── mod.rs ├── errors.rs ├── events ├── binlog_event.rs ├── event_header.rs ├── event_parser.rs ├── event_type.rs ├── format_description_event.rs ├── heartbeat_event.rs ├── intvar_event.rs ├── mod.rs ├── query_event.rs ├── rotate_event.rs ├── row_events │ ├── actual_string_type.rs │ ├── col_parser.rs │ ├── decimal.rs │ ├── delete_rows_event.rs │ ├── mod.rs │ ├── mysql_value.rs │ ├── row_data.rs │ ├── row_parser.rs │ ├── update_rows_event.rs │ └── write_rows_event.rs ├── rows_query_event.rs ├── table_map_event.rs ├── uservar_event.rs └── xid_event.rs ├── extensions.rs ├── lib.rs ├── metadata ├── default_charset.rs ├── metadata_type.rs ├── mod.rs └── table_metadata.rs ├── packet_channel.rs ├── providers ├── mariadb │ ├── events │ │ ├── gtid_event.rs │ │ ├── gtid_list_event.rs │ │ └── mod.rs │ ├── gtid │ │ ├── gtid.rs │ │ ├── gtid_list.rs │ │ └── mod.rs │ ├── mariadb_provider.rs │ └── mod.rs ├── mod.rs └── mysql │ ├── events │ ├── gtid_event.rs │ ├── mod.rs │ └── prev_gtids_event.rs │ ├── gtid │ ├── gtid.rs │ ├── gtid_set.rs │ ├── interval.rs │ ├── mod.rs │ ├── uuid.rs │ └── uuid_set.rs │ ├── mod.rs │ └── mysql_provider.rs ├── replica_options.rs ├── responses ├── auth_switch_packet.rs ├── end_of_file_packet.rs ├── error_packet.rs ├── handshake_packet.rs ├── mod.rs ├── response_type.rs └── result_set_row_packet.rs ├── ssl_mode.rs └── starting_strategy.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | .idea -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mysql_cdc" 3 | version = "0.2.1" 4 | edition = "2021" 5 | authors = ["Ruslan Ulianets "] 6 | description = "MySQL/MariaDB binlog change data capture (CDC) connector for Rust" 7 | homepage = "https://github.com/rusuly/mysql_cdc" 8 | repository = "https://github.com/rusuly/mysql_cdc" 9 | license = "MIT" 10 | 11 | [dependencies] 12 | byteorder = "1.4.3" 13 | sha1 = "0.10.5" 14 | sha2 = "0.10.6" 15 | openssl = "0.10.54" 16 | hex = "0.4.3" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ruslan Ulianets 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mysql_cdc 2 | 3 | [![crates.io](https://img.shields.io/crates/v/mysql_cdc.svg)](https://crates.io/crates/mysql_cdc) 4 | [![docs.rs](https://docs.rs/mysql_cdc/badge.svg)](https://docs.rs/mysql_cdc) 5 | 6 | MySQL/MariaDB binlog replication client for Rust 7 | 8 | ## Limitations 9 | Please note the lib currently has the following limitations: 10 | - Supports only standard auth plugins `mysql_native_password` and `caching_sha2_password`. 11 | - **Currently, the library doesn't support SSL encryption.** 12 | - **Doesn't handle split packets (16MB and more).** 13 | 14 | ## Binlog event stream replication 15 | Real-time replication client works the following way. 16 | ```rust 17 | use mysql_cdc::binlog_client::BinlogClient; 18 | use mysql_cdc::binlog_options::BinlogOptions; 19 | use mysql_cdc::errors::Error; 20 | use mysql_cdc::providers::mariadb::gtid::gtid_list::GtidList; 21 | use mysql_cdc::providers::mysql::gtid::gtid_set::GtidSet; 22 | use mysql_cdc::replica_options::ReplicaOptions; 23 | use mysql_cdc::ssl_mode::SslMode; 24 | 25 | fn main() -> Result<(), Error> { 26 | // Start replication from MariaDB GTID 27 | let _options = BinlogOptions::from_mariadb_gtid(GtidList::parse("0-1-270")?); 28 | 29 | // Start replication from MySQL GTID 30 | let gtid_set = 31 | "d4c17f0c-4f11-11ea-93e3-325d3e1cd1c8:1-107, f442510a-2881-11ea-b1dd-27916133dbb2:1-7"; 32 | let _options = BinlogOptions::from_mysql_gtid(GtidSet::parse(gtid_set)?); 33 | 34 | // Start replication from the position 35 | let _options = BinlogOptions::from_position(String::from("mysql-bin.000008"), 195); 36 | 37 | // Start replication from last master position. 38 | // Useful when you are only interested in new changes. 39 | let _options = BinlogOptions::from_end(); 40 | 41 | // Start replication from first event of first available master binlog. 42 | // Note that binlog files by default have expiration time and deleted. 43 | let options = BinlogOptions::from_start(); 44 | 45 | let options = ReplicaOptions { 46 | username: String::from("root"), 47 | password: String::from("Qwertyu1"), 48 | blocking: true, 49 | ssl_mode: SslMode::Disabled, 50 | binlog: options, 51 | ..Default::default() 52 | }; 53 | 54 | let mut client = BinlogClient::new(options); 55 | 56 | for result in client.replicate()? { 57 | let (header, event) = result?; 58 | println!("{:#?}", header); 59 | println!("{:#?}", event); 60 | 61 | // You process an event here 62 | 63 | // After you processed the event, you need to update replication position 64 | client.commit(&header, &event); 65 | } 66 | Ok(()) 67 | } 68 | ``` 69 | A typical transaction has the following structure. 70 | 1. `GtidEvent` if gtid mode is enabled. 71 | 2. One or many `TableMapEvent` events. 72 | - One or many `WriteRowsEvent` events. 73 | - One or many `UpdateRowsEvent` events. 74 | - One or many `DeleteRowsEvent` events. 75 | 3. `XidEvent` indicating commit of the transaction. 76 | 77 | **It's best practice to use GTID replication with the `from_gtid` method.** Using the approach you can correctly perform replication failover. 78 | Note that in GTID mode `from_gtid` has the following behavior: 79 | - `from_gtid(@@gtid_purged)` acts like `from_start()` 80 | - `from_gtid(@@gtid_executed)` acts like `from_end()` 81 | 82 | ## Reading binlog files offline 83 | In some cases you will need to read binlog files offline from the file system. 84 | This can be done using `BinlogReader` class. 85 | ```rust 86 | use mysql_cdc::{binlog_reader::BinlogReader, errors::Error}; 87 | use std::fs::File; 88 | 89 | const PATH: &str = "mysql-bin.000001"; 90 | 91 | fn main() -> Result<(), Error> { 92 | let file = File::open(PATH)?; 93 | let reader = BinlogReader::new(file)?; 94 | 95 | for result in reader.read_events() { 96 | let (header, event) = result?; 97 | println!("{:#?}", header); 98 | println!("{:#?}", event); 99 | } 100 | Ok(()) 101 | } 102 | ``` 103 | -------------------------------------------------------------------------------- /examples/binlog_client.rs: -------------------------------------------------------------------------------- 1 | use mysql_cdc::binlog_client::BinlogClient; 2 | use mysql_cdc::binlog_options::BinlogOptions; 3 | use mysql_cdc::errors::Error; 4 | use mysql_cdc::providers::mariadb::gtid::gtid_list::GtidList; 5 | use mysql_cdc::providers::mysql::gtid::gtid_set::GtidSet; 6 | use mysql_cdc::replica_options::ReplicaOptions; 7 | use mysql_cdc::ssl_mode::SslMode; 8 | 9 | fn main() -> Result<(), Error> { 10 | // Start replication from MariaDB GTID 11 | let _options = BinlogOptions::from_mariadb_gtid(GtidList::parse("0-1-270")?); 12 | 13 | // Start replication from MySQL GTID 14 | let gtid_set = 15 | "d4c17f0c-4f11-11ea-93e3-325d3e1cd1c8:1-107, f442510a-2881-11ea-b1dd-27916133dbb2:1-7"; 16 | let _options = BinlogOptions::from_mysql_gtid(GtidSet::parse(gtid_set)?); 17 | 18 | // Start replication from the position 19 | let _options = BinlogOptions::from_position(String::from("mysql-bin.000008"), 195); 20 | 21 | // Start replication from last master position. 22 | // Useful when you are only interested in new changes. 23 | let _options = BinlogOptions::from_end(); 24 | 25 | // Start replication from first event of first available master binlog. 26 | // Note that binlog files by default have expiration time and deleted. 27 | let options = BinlogOptions::from_start(); 28 | 29 | let options = ReplicaOptions { 30 | username: String::from("root"), 31 | password: String::from("Qwertyu1"), 32 | blocking: true, 33 | ssl_mode: SslMode::Disabled, 34 | binlog: options, 35 | ..Default::default() 36 | }; 37 | 38 | let mut client = BinlogClient::new(options); 39 | 40 | for result in client.replicate()? { 41 | let (header, event) = result?; 42 | println!("Header: {:#?}", header); 43 | println!("Event: {:#?}", event); 44 | 45 | println!("Replication position before event processed"); 46 | print_position(&client); 47 | 48 | // After you processed the event, you need to update replication position 49 | client.commit(&header, &event); 50 | 51 | println!("Replication position after event processed"); 52 | print_position(&client); 53 | } 54 | Ok(()) 55 | } 56 | 57 | fn print_position(client: &BinlogClient) { 58 | println!("Binlog Filename: {:#?}", client.options.binlog.filename); 59 | println!("Binlog Position: {:#?}", client.options.binlog.position); 60 | 61 | if let Some(x) = &client.options.binlog.gtid_list { 62 | println!("MariaDB Gtid Position: {:#?}", x.to_string()); 63 | } 64 | if let Some(x) = &client.options.binlog.gtid_set { 65 | println!("MySQL Gtid Position: {:#?}", x.to_string()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/binlog_reader.rs: -------------------------------------------------------------------------------- 1 | use mysql_cdc::{binlog_reader::BinlogReader, errors::Error}; 2 | use std::fs::File; 3 | 4 | const PATH: &str = "mysql-bin.000001"; 5 | 6 | fn main() -> Result<(), Error> { 7 | let file = File::open(PATH)?; 8 | let reader = BinlogReader::new(file)?; 9 | 10 | for result in reader.read_events() { 11 | let (header, event) = result?; 12 | println!("{:#?}", header); 13 | println!("{:#?}", event); 14 | } 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /src/binlog_client.rs: -------------------------------------------------------------------------------- 1 | use crate::binlog_events::BinlogEvents; 2 | use crate::constants::database_provider::DatabaseProvider; 3 | use crate::errors::Error; 4 | use crate::events::binlog_event::BinlogEvent; 5 | use crate::events::event_header::EventHeader; 6 | use crate::providers::mariadb::gtid::gtid::Gtid as MariaGtid; 7 | use crate::providers::mariadb::mariadb_provider::replicate_mariadb; 8 | use crate::providers::mysql::gtid::gtid::Gtid as MySqlGtid; 9 | use crate::providers::mysql::mysql_provider::replicate_mysql; 10 | use crate::replica_options::ReplicaOptions; 11 | use crate::ssl_mode::SslMode; 12 | use crate::starting_strategy::StartingStrategy; 13 | 14 | /// MySql replication client streaming binlog events in real-time. 15 | pub struct BinlogClient { 16 | pub options: ReplicaOptions, 17 | transaction: bool, 18 | maria_gtid: Option, 19 | mysql_gtid: Option, 20 | } 21 | 22 | impl BinlogClient { 23 | pub fn new(options: ReplicaOptions) -> Self { 24 | if options.ssl_mode != SslMode::Disabled { 25 | unimplemented!("Ssl encryption is not supported in this version"); 26 | } 27 | 28 | Self { 29 | options, 30 | transaction: false, 31 | maria_gtid: None, 32 | mysql_gtid: None, 33 | } 34 | } 35 | 36 | /// Replicates binlog events from the server 37 | pub fn replicate(&mut self) -> Result { 38 | let (mut channel, provider) = self.connect()?; 39 | 40 | // Reset on reconnect 41 | self.transaction = false; 42 | self.maria_gtid = None; 43 | self.mysql_gtid = None; 44 | 45 | self.adjust_starting_position(&mut channel)?; 46 | self.set_master_heartbeat(&mut channel)?; 47 | let checksum = self.set_master_binlog_checksum(&mut channel)?; 48 | 49 | let server_id = if self.options.blocking { 50 | self.options.server_id 51 | } else { 52 | 0 53 | }; 54 | 55 | match provider { 56 | DatabaseProvider::MariaDB => replicate_mariadb(&mut channel, &self.options, server_id)?, 57 | DatabaseProvider::MySQL => replicate_mysql(&mut channel, &self.options, server_id)?, 58 | } 59 | 60 | Ok(BinlogEvents::new(channel, checksum)) 61 | } 62 | 63 | /// Updates current replication position 64 | pub fn commit(&mut self, header: &EventHeader, event: &BinlogEvent) { 65 | self.update_gtid_position(event); 66 | self.update_binlog_position(header, event); 67 | } 68 | 69 | fn update_gtid_position(&mut self, event: &BinlogEvent) { 70 | if self.options.binlog.starting_strategy != StartingStrategy::FromGtid { 71 | return; 72 | } 73 | 74 | match event { 75 | BinlogEvent::MariaDbGtidEvent(x) => { 76 | self.maria_gtid = Some(x.gtid.clone()); 77 | } 78 | BinlogEvent::MySqlGtidEvent(x) => { 79 | self.mysql_gtid = Some(x.gtid.clone()); 80 | } 81 | BinlogEvent::XidEvent(_) => { 82 | self.commit_gtid(); 83 | } 84 | BinlogEvent::QueryEvent(x) => { 85 | if x.sql_statement.is_empty() { 86 | return; 87 | } 88 | if x.sql_statement == "BEGIN" { 89 | self.transaction = true; 90 | } else if x.sql_statement == "COMMIT" || x.sql_statement == "ROLLBACK" { 91 | self.commit_gtid(); 92 | } else if !self.transaction { 93 | // Auto-commit query like DDL 94 | self.commit_gtid(); 95 | } 96 | } 97 | _ => {} 98 | } 99 | } 100 | 101 | fn update_binlog_position(&mut self, header: &EventHeader, event: &BinlogEvent) { 102 | // Rows event depends on preceding TableMapEvent & we change the position 103 | // after we read them atomically to prevent missing mapping on reconnect. 104 | // Figure out something better as TableMapEvent can be followed by several row events. 105 | match event { 106 | BinlogEvent::TableMapEvent(_) => return, 107 | BinlogEvent::RotateEvent(x) => { 108 | self.options.binlog.filename = x.binlog_filename.clone(); 109 | self.options.binlog.position = x.binlog_position as u32; 110 | } 111 | _ => { 112 | if header.next_event_position > 0 { 113 | self.options.binlog.position = header.next_event_position; 114 | } 115 | } 116 | } 117 | } 118 | 119 | fn commit_gtid(&mut self) { 120 | self.transaction = false; 121 | 122 | if let Some(gtid) = &self.maria_gtid { 123 | if let Some(list) = &mut self.options.binlog.gtid_list { 124 | list.add_gtid(gtid.clone()); 125 | } 126 | } 127 | if let Some(gtid) = &self.mysql_gtid { 128 | if let Some(set) = &mut self.options.binlog.gtid_set { 129 | set.add_gtid(gtid.clone()).unwrap(); 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/binlog_events.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::checksum_type::ChecksumType; 2 | use crate::constants::EVENT_HEADER_SIZE; 3 | use crate::errors::Error; 4 | use crate::events::binlog_event::BinlogEvent; 5 | use crate::events::event_header::EventHeader; 6 | use crate::events::event_parser::EventParser; 7 | use crate::packet_channel::PacketChannel; 8 | use crate::responses::end_of_file_packet::EndOfFilePacket; 9 | use crate::responses::error_packet::ErrorPacket; 10 | use crate::responses::response_type::ResponseType; 11 | 12 | pub struct BinlogEvents { 13 | pub channel: PacketChannel, 14 | pub parser: EventParser, 15 | } 16 | 17 | impl BinlogEvents { 18 | pub fn new(channel: PacketChannel, checksum: ChecksumType) -> Self { 19 | let mut parser = EventParser::new(); 20 | parser.checksum_type = checksum; 21 | 22 | Self { channel, parser } 23 | } 24 | 25 | pub fn read_event(&mut self, packet: &[u8]) -> Result<(EventHeader, BinlogEvent), Error> { 26 | let header = EventHeader::parse(&packet[1..])?; 27 | let event_slice = &packet[1 + EVENT_HEADER_SIZE..]; 28 | let event = self.parser.parse_event(&header, event_slice)?; 29 | Ok((header, event)) 30 | } 31 | 32 | pub fn read_error(&mut self, packet: &[u8]) -> Result<(EventHeader, BinlogEvent), Error> { 33 | let error = ErrorPacket::parse(&packet[1..])?; 34 | Err(Error::String(format!("Event stream error. {:?}", error))) 35 | } 36 | } 37 | 38 | impl Iterator for BinlogEvents { 39 | type Item = Result<(EventHeader, BinlogEvent), Error>; 40 | 41 | /// Reads binlog event packets from network stream. 42 | /// See more 43 | fn next(&mut self) -> Option { 44 | let (packet, _) = match self.channel.read_packet() { 45 | Ok(x) => x, 46 | Err(e) => return Some(Err(Error::IoError(e))), 47 | }; 48 | match packet[0] { 49 | ResponseType::OK => Some(self.read_event(&packet)), 50 | ResponseType::ERROR => Some(self.read_error(&packet)), 51 | ResponseType::END_OF_FILE => { 52 | let _ = EndOfFilePacket::parse(&packet[1..]); 53 | None 54 | } 55 | _ => Some(Err(Error::String( 56 | "Unknown network stream status".to_string(), 57 | ))), 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/binlog_options.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::FIRST_EVENT_POSITION; 2 | use crate::providers::mariadb::gtid::gtid_list::GtidList; 3 | use crate::providers::mysql::gtid::gtid_set::GtidSet; 4 | use crate::starting_strategy::StartingStrategy; 5 | 6 | /// Replication options used when client connects to the server. 7 | #[derive(Debug)] 8 | pub struct BinlogOptions { 9 | /// Binary log file name. 10 | /// The value is automatically changed on the RotateEvent. 11 | /// On reconnect the client resumes replication from the current position. 12 | pub filename: String, 13 | 14 | /// Binary log file position. 15 | /// The value is automatically changed when an event is successfully processed by a client. 16 | /// On reconnect the client resumes replication from the current position. 17 | pub position: u32, 18 | 19 | /// MySQL Global Transaction ID position to start replication from. 20 | /// See MySQL GTID 21 | pub gtid_set: Option, 22 | 23 | /// MariaDB Global Transaction ID position to start replication from. 24 | /// See MariaDB GTID 25 | pub gtid_list: Option, 26 | 27 | /// Gets replication starting strategy. 28 | pub starting_strategy: StartingStrategy, 29 | } 30 | 31 | impl BinlogOptions { 32 | /// Starts replication from first available binlog on master server. 33 | pub fn from_start() -> BinlogOptions { 34 | BinlogOptions { 35 | filename: String::new(), 36 | position: FIRST_EVENT_POSITION as u32, 37 | gtid_set: None, 38 | gtid_list: None, 39 | starting_strategy: StartingStrategy::FromStart, 40 | } 41 | } 42 | 43 | /// Starts replication from last master binlog position 44 | /// which will be read by BinlogClient on first connect. 45 | pub fn from_end() -> BinlogOptions { 46 | BinlogOptions { 47 | filename: String::new(), 48 | position: 0, 49 | gtid_set: None, 50 | gtid_list: None, 51 | starting_strategy: StartingStrategy::FromEnd, 52 | } 53 | } 54 | 55 | /// Starts replication from specified binlog filename and position. 56 | pub fn from_position(filename: String, position: u32) -> BinlogOptions { 57 | BinlogOptions { 58 | filename, 59 | position, 60 | gtid_set: None, 61 | gtid_list: None, 62 | starting_strategy: StartingStrategy::FromPosition, 63 | } 64 | } 65 | 66 | /// Starts replication from specified Global Transaction ID. 67 | pub fn from_mysql_gtid(gtid_set: GtidSet) -> BinlogOptions { 68 | BinlogOptions { 69 | filename: String::new(), 70 | position: FIRST_EVENT_POSITION as u32, 71 | gtid_set: Some(gtid_set), 72 | gtid_list: None, 73 | starting_strategy: StartingStrategy::FromGtid, 74 | } 75 | } 76 | 77 | pub fn from_mariadb_gtid(gtid_list: GtidList) -> BinlogOptions { 78 | BinlogOptions { 79 | filename: String::new(), 80 | position: FIRST_EVENT_POSITION as u32, 81 | gtid_set: None, 82 | gtid_list: Some(gtid_list), 83 | starting_strategy: StartingStrategy::FromGtid, 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/binlog_reader.rs: -------------------------------------------------------------------------------- 1 | use crate::constants; 2 | use crate::errors::Error; 3 | use crate::events::binlog_event::BinlogEvent; 4 | use crate::events::event_header::EventHeader; 5 | use crate::events::event_parser::EventParser; 6 | use constants::EVENT_HEADER_SIZE; 7 | use std::fs::File; 8 | use std::io::{ErrorKind, Read}; 9 | 10 | const MAGIC_NUMBER: [u8; constants::FIRST_EVENT_POSITION] = [0xfe, 0x62, 0x69, 0x6e]; 11 | 12 | /// Reads binlog events from a stream. 13 | pub struct BinlogReader { 14 | stream: File, 15 | parser: EventParser, 16 | payload_buffer: Vec, 17 | } 18 | 19 | impl BinlogReader { 20 | pub fn new(mut stream: File) -> Result { 21 | let mut header = [0; constants::FIRST_EVENT_POSITION]; 22 | stream.read_exact(&mut header)?; 23 | 24 | if header != MAGIC_NUMBER { 25 | return Err(Error::String("Invalid binary log file header".to_string())); 26 | } 27 | 28 | Ok(Self { 29 | stream, 30 | parser: EventParser::new(), 31 | payload_buffer: vec![0; constants::PAYLOAD_BUFFER_SIZE], 32 | }) 33 | } 34 | 35 | pub fn read_events(self) -> Self { 36 | self 37 | } 38 | 39 | pub fn read_event(&mut self) -> Result<(EventHeader, BinlogEvent), Error> { 40 | // Parse header 41 | let mut header_buffer = [0; EVENT_HEADER_SIZE]; 42 | self.stream.read_exact(&mut header_buffer)?; 43 | let header = EventHeader::parse(&header_buffer)?; 44 | 45 | let payload_length = header.event_length as usize - EVENT_HEADER_SIZE; 46 | if payload_length as usize > constants::PAYLOAD_BUFFER_SIZE { 47 | let mut vec: Vec = vec![0; payload_length]; 48 | 49 | self.stream.read_exact(&mut vec)?; 50 | let binlog_event = self.parser.parse_event(&header, &vec)?; 51 | Ok((header, binlog_event)) 52 | } else { 53 | let slice = &mut self.payload_buffer[0..payload_length]; 54 | 55 | self.stream.read_exact(slice)?; 56 | let binlog_event = self.parser.parse_event(&header, slice)?; 57 | Ok((header, binlog_event)) 58 | } 59 | } 60 | } 61 | 62 | impl Iterator for BinlogReader { 63 | type Item = Result<(EventHeader, BinlogEvent), Error>; 64 | 65 | fn next(&mut self) -> Option { 66 | let result = self.read_event(); 67 | if let Err(error) = &result { 68 | if let Error::IoError(io_error) = error { 69 | if let ErrorKind::UnexpectedEof = io_error.kind() { 70 | return None; 71 | } 72 | } 73 | } 74 | Some(result) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/commands/auth_plugin_switch_command.rs: -------------------------------------------------------------------------------- 1 | use crate::{constants::auth_plugin_names::AuthPlugin, extensions::encrypt_password}; 2 | use std::io::{self, Cursor, Write}; 3 | 4 | pub struct AuthPluginSwitchCommand { 5 | pub password: String, 6 | pub scramble: String, 7 | pub auth_plugin_name: String, 8 | pub auth_plugin: AuthPlugin, 9 | } 10 | 11 | impl AuthPluginSwitchCommand { 12 | pub fn new( 13 | password: &String, 14 | scramble: &String, 15 | auth_plugin_name: &String, 16 | auth_plugin: AuthPlugin, 17 | ) -> Self { 18 | Self { 19 | password: password.clone(), 20 | scramble: scramble.clone(), 21 | auth_plugin_name: auth_plugin_name.clone(), 22 | auth_plugin: auth_plugin, 23 | } 24 | } 25 | 26 | pub fn serialize(&self) -> Result, io::Error> { 27 | let mut vec = Vec::new(); 28 | let mut cursor = Cursor::new(&mut vec); 29 | 30 | let encrypted_password = 31 | encrypt_password(&self.password, &self.scramble, &self.auth_plugin); 32 | cursor.write(&encrypted_password)?; 33 | 34 | Ok(vec) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/authenticate_command.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, WriteBytesExt}; 2 | use std::io::{self, Cursor, Write}; 3 | 4 | use crate::constants::auth_plugin_names::AuthPlugin; 5 | use crate::constants::capability_flags; 6 | use crate::extensions::{encrypt_password, write_null_term_string}; 7 | use crate::replica_options::ReplicaOptions; 8 | use crate::responses::handshake_packet::HandshakePacket; 9 | 10 | /// Client handshake response to the server initial handshake packet. 11 | /// See more 12 | pub struct AuthenticateCommand { 13 | pub client_capabilities: u32, 14 | pub max_packet_size: u32, 15 | pub client_collation: u8, 16 | pub username: String, 17 | pub password: String, 18 | pub database: Option, 19 | pub scramble: String, 20 | pub auth_plugin: AuthPlugin, 21 | pub auth_plugin_name: String, 22 | } 23 | 24 | impl AuthenticateCommand { 25 | pub fn new( 26 | options: &ReplicaOptions, 27 | handshake: &HandshakePacket, 28 | auth_plugin: AuthPlugin, 29 | client_collation: u8, 30 | ) -> Self { 31 | let mut client_capabilities = capability_flags::LONG_FLAG 32 | | capability_flags::PROTOCOL_41 33 | | capability_flags::SECURE_CONNECTION 34 | | capability_flags::PLUGIN_AUTH; 35 | 36 | if let Some(_x) = &options.database { 37 | client_capabilities |= capability_flags::CONNECT_WITH_DB; 38 | } 39 | 40 | let client_capabilities = client_capabilities as u32; 41 | 42 | Self { 43 | client_capabilities, 44 | max_packet_size: 0, 45 | client_collation, 46 | username: options.username.clone(), 47 | password: options.password.clone(), 48 | database: options.database.clone(), 49 | scramble: handshake.scramble.clone(), 50 | auth_plugin_name: handshake.auth_plugin_name.clone(), 51 | auth_plugin: auth_plugin, 52 | } 53 | } 54 | 55 | pub fn serialize(&self) -> Result, io::Error> { 56 | let mut vec = Vec::new(); 57 | let mut cursor = Cursor::new(&mut vec); 58 | 59 | cursor.write_u32::(self.client_capabilities)?; 60 | cursor.write_u32::(self.max_packet_size)?; 61 | cursor.write_u8(self.client_collation)?; 62 | 63 | // Fill reserved bytes 64 | for _number in 0..23 { 65 | cursor.write_u8(0)?; 66 | } 67 | 68 | write_null_term_string(&mut cursor, &self.username)?; 69 | 70 | let encrypted_password = 71 | encrypt_password(&self.password, &self.scramble, &self.auth_plugin); 72 | cursor.write_u8(encrypted_password.len() as u8)?; 73 | cursor.write(&encrypted_password)?; 74 | 75 | if let Some(database) = &self.database { 76 | write_null_term_string(&mut cursor, database)?; 77 | } 78 | 79 | write_null_term_string(&mut cursor, &self.auth_plugin_name)?; 80 | Ok(vec) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/commands/command_type.rs: -------------------------------------------------------------------------------- 1 | /// Command types are included in body headers to identify the commands 2 | #[allow(dead_code)] 3 | pub enum CommandType { 4 | Sleep = 0, 5 | Quit = 1, 6 | InitDb = 2, 7 | Query = 3, 8 | FieldList = 4, 9 | CreateDb = 5, 10 | DropDb = 6, 11 | Refresh = 7, 12 | Shutdown = 8, 13 | Statistics = 9, 14 | ProcessInfo = 10, 15 | Connect = 11, 16 | ProcessKill = 12, 17 | Debug = 13, 18 | Ping = 14, 19 | Time = 15, 20 | DelayedInsert = 16, 21 | ChangeUser = 17, 22 | BinlogDump = 18, 23 | 24 | RegisterSlave = 21, 25 | BinlogDumpGtid = 30, 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/dump_binlog_command.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::command_type::CommandType; 2 | use byteorder::{LittleEndian, WriteBytesExt}; 3 | use std::io::{self, Cursor, Write}; 4 | 5 | /// Requests binlog event stream. 6 | /// See more 7 | pub struct DumpBinlogCommand { 8 | pub server_id: u32, 9 | pub binlog_filename: String, 10 | pub binlog_position: u32, 11 | pub flags: u16, 12 | } 13 | 14 | impl DumpBinlogCommand { 15 | pub fn new(server_id: u32, binlog_filename: String, binlog_position: u32) -> Self { 16 | Self { 17 | server_id, 18 | binlog_filename, 19 | binlog_position, 20 | flags: 0, 21 | } 22 | } 23 | 24 | pub fn serialize(&self) -> Result, io::Error> { 25 | let mut vec = Vec::new(); 26 | let mut cursor = Cursor::new(&mut vec); 27 | 28 | cursor.write_u8(CommandType::BinlogDump as u8)?; 29 | cursor.write_u32::(self.binlog_position)?; 30 | cursor.write_u16::(self.flags)?; 31 | cursor.write_u32::(self.server_id)?; 32 | cursor.write(self.binlog_filename.as_bytes())?; 33 | 34 | Ok(vec) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/dump_binlog_gtid_command.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::command_type::CommandType; 2 | use crate::providers::mysql::gtid::gtid_set::GtidSet; 3 | use byteorder::{LittleEndian, WriteBytesExt}; 4 | use std::io::{self, Cursor, Write}; 5 | 6 | /// Requests binlog event stream by GtidSet. 7 | /// See more 8 | pub struct DumpBinlogGtidCommand { 9 | pub server_id: u32, 10 | pub binlog_filename: String, 11 | pub binlog_position: u32, 12 | pub flags: u16, 13 | } 14 | 15 | impl DumpBinlogGtidCommand { 16 | pub fn new(server_id: u32, binlog_filename: String, binlog_position: u32) -> Self { 17 | Self { 18 | server_id, 19 | binlog_filename, 20 | binlog_position, 21 | flags: 0, 22 | } 23 | } 24 | 25 | pub fn serialize(&self, gtid_set: &GtidSet) -> Result, io::Error> { 26 | let mut vec = Vec::new(); 27 | let mut cursor = Cursor::new(&mut vec); 28 | 29 | cursor.write_u8(CommandType::BinlogDumpGtid as u8)?; 30 | cursor.write_u16::(self.flags)?; 31 | cursor.write_u32::(self.server_id)?; 32 | 33 | let filename_len = self.binlog_filename.len() as u32; 34 | cursor.write_u32::(filename_len)?; 35 | cursor.write(self.binlog_filename.as_bytes())?; 36 | 37 | let position = self.binlog_position as u64; 38 | cursor.write_u64::(position)?; 39 | 40 | let mut data_length = 8; /* Number of uuid_sets */ 41 | for uuid_set in gtid_set.uuid_sets.values() { 42 | data_length += 16; /* SourceId */ 43 | data_length += 8; /* Number of intervals */ 44 | data_length += uuid_set.intervals.len() * (8 + 8) /* Start-End */; 45 | } 46 | 47 | cursor.write_u32::(data_length as u32)?; 48 | cursor.write_u64::(gtid_set.uuid_sets.len() as u64)?; 49 | 50 | for uuid_set in gtid_set.uuid_sets.values() { 51 | cursor.write(&uuid_set.source_id.data)?; 52 | cursor.write_u64::(uuid_set.intervals.len() as u64)?; 53 | 54 | for interval in &uuid_set.intervals { 55 | cursor.write_u64::(interval.start)?; 56 | cursor.write_u64::(interval.end + 1)?; 57 | } 58 | } 59 | 60 | Ok(vec) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth_plugin_switch_command; 2 | pub mod authenticate_command; 3 | pub mod command_type; 4 | pub mod dump_binlog_command; 5 | pub mod dump_binlog_gtid_command; 6 | pub mod query_command; 7 | pub mod register_slave_command; 8 | pub mod ssl_request_command; 9 | -------------------------------------------------------------------------------- /src/commands/query_command.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::command_type::CommandType; 2 | use byteorder::WriteBytesExt; 3 | use std::io::{self, Cursor, Write}; 4 | 5 | /// COM_QUERY sends the server an SQL statement to be executed immediately. 6 | /// See more 7 | pub struct QueryCommand { 8 | pub sql: String, 9 | } 10 | 11 | impl QueryCommand { 12 | pub fn new(sql: String) -> Self { 13 | Self { sql } 14 | } 15 | 16 | pub fn serialize(&self) -> Result, io::Error> { 17 | let mut vec = Vec::new(); 18 | let mut cursor = Cursor::new(&mut vec); 19 | 20 | cursor.write_u8(CommandType::Query as u8)?; 21 | cursor.write(self.sql.as_bytes())?; 22 | 23 | Ok(vec) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/register_slave_command.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::command_type::CommandType; 2 | use byteorder::{LittleEndian, WriteBytesExt}; 3 | use std::io::{self, Cursor}; 4 | 5 | /// Used for MariaDB Gtid replication. 6 | /// See MariaDB docs 7 | /// See MySQL docs 8 | pub struct RegisterSlaveCommand { 9 | pub server_id: u32, 10 | } 11 | 12 | impl RegisterSlaveCommand { 13 | pub fn new(server_id: u32) -> Self { 14 | Self { server_id } 15 | } 16 | 17 | pub fn serialize(&self) -> Result, io::Error> { 18 | let mut vec = Vec::new(); 19 | let mut cursor = Cursor::new(&mut vec); 20 | 21 | cursor.write_u8(CommandType::RegisterSlave as u8)?; 22 | cursor.write_u32::(self.server_id)?; 23 | 24 | //Empty host, user, password, port, rank, masterid 25 | cursor.write_u8(0)?; 26 | cursor.write_u8(0)?; 27 | cursor.write_u8(0)?; 28 | cursor.write_u16::(0)?; 29 | cursor.write_u32::(0)?; 30 | cursor.write_u32::(0)?; 31 | 32 | Ok(vec) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/ssl_request_command.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, WriteBytesExt}; 2 | use std::io::{self, Cursor}; 3 | 4 | use crate::constants::capability_flags; 5 | 6 | /// SSLRequest packet used in SSL/TLS connection. 7 | /// See more 8 | pub struct SslRequestCommand { 9 | pub client_capabilities: u32, 10 | pub max_packet_size: u32, 11 | pub client_collation: u8, 12 | } 13 | 14 | impl SslRequestCommand { 15 | pub fn new(client_collation: u8) -> Self { 16 | let client_capabilities = capability_flags::LONG_FLAG 17 | | capability_flags::PROTOCOL_41 18 | | capability_flags::SECURE_CONNECTION 19 | | capability_flags::SSL 20 | | capability_flags::PLUGIN_AUTH; 21 | 22 | let client_capabilities = client_capabilities as u32; 23 | 24 | Self { 25 | client_capabilities, 26 | max_packet_size: 0, 27 | client_collation, 28 | } 29 | } 30 | 31 | pub fn serialize(&self) -> Result, io::Error> { 32 | let mut vec = Vec::new(); 33 | let mut cursor = Cursor::new(&mut vec); 34 | 35 | cursor.write_u32::(self.client_capabilities)?; 36 | cursor.write_u32::(self.max_packet_size)?; 37 | cursor.write_u8(self.client_collation)?; 38 | 39 | // Fill reserved bytes 40 | for _number in 0..23 { 41 | cursor.write_u8(0)?; 42 | } 43 | 44 | Ok(vec) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/configure.rs: -------------------------------------------------------------------------------- 1 | use crate::binlog_client::BinlogClient; 2 | use crate::commands::query_command::QueryCommand; 3 | use crate::constants::checksum_type::ChecksumType; 4 | use crate::errors::Error; 5 | use crate::extensions::check_error_packet; 6 | use crate::packet_channel::PacketChannel; 7 | use crate::responses::response_type::ResponseType; 8 | use crate::responses::result_set_row_packet::ResultSetRowPacket; 9 | use crate::starting_strategy::StartingStrategy; 10 | 11 | impl BinlogClient { 12 | pub fn adjust_starting_position(&mut self, channel: &mut PacketChannel) -> Result<(), Error> { 13 | if self.options.binlog.starting_strategy != StartingStrategy::FromEnd { 14 | return Ok(()); 15 | } 16 | 17 | // Ignore if position was read before in case of reconnect. 18 | if !self.options.binlog.filename.is_empty() { 19 | return Ok(()); 20 | } 21 | 22 | let command = QueryCommand::new("show master status".to_string()); 23 | channel.write_packet(&command.serialize()?, 0)?; 24 | 25 | let result_set = self.read_result_set(channel)?; 26 | if result_set.len() != 1 { 27 | return Err(Error::String( 28 | "Could not read master binlog position.".to_string(), 29 | )); 30 | } 31 | 32 | self.options.binlog.filename = result_set[0].cells[0].clone(); 33 | self.options.binlog.position = result_set[0].cells[1].parse()?; 34 | Ok(()) 35 | } 36 | 37 | pub fn set_master_heartbeat(&mut self, channel: &mut PacketChannel) -> Result<(), Error> { 38 | let milliseconds = self.options.heartbeat_interval.as_millis(); 39 | let nanoseconds = milliseconds * 1000 * 1000; 40 | let query = format!("set @master_heartbeat_period={}", nanoseconds); 41 | let command = QueryCommand::new(query.to_string()); 42 | channel.write_packet(&command.serialize()?, 0)?; 43 | let (packet, _) = channel.read_packet()?; 44 | check_error_packet(&packet, "Setting master heartbeat error.")?; 45 | Ok(()) 46 | } 47 | 48 | pub fn set_master_binlog_checksum( 49 | &mut self, 50 | channel: &mut PacketChannel, 51 | ) -> Result { 52 | let command = 53 | QueryCommand::new("SET @master_binlog_checksum= @@global.binlog_checksum".to_string()); 54 | channel.write_packet(&command.serialize()?, 0)?; 55 | let (packet, _) = channel.read_packet()?; 56 | check_error_packet(&packet, "Setting master_binlog_checksum error.")?; 57 | 58 | let command = QueryCommand::new("SELECT @master_binlog_checksum".to_string()); 59 | channel.write_packet(&command.serialize()?, 0)?; 60 | let result_set = self.read_result_set(channel)?; 61 | 62 | // When replication is started fake RotateEvent comes before FormatDescriptionEvent. 63 | // In order to deserialize the event we have to obtain checksum type length in advance. 64 | Ok(ChecksumType::from_name(&result_set[0].cells[0])?) 65 | } 66 | 67 | fn read_result_set( 68 | &self, 69 | channel: &mut PacketChannel, 70 | ) -> Result, Error> { 71 | let (packet, _) = channel.read_packet()?; 72 | check_error_packet(&packet, "Reading result set error.")?; 73 | 74 | loop { 75 | // Skip through metadata 76 | let (packet, _) = channel.read_packet()?; 77 | if packet[0] == ResponseType::END_OF_FILE { 78 | break; 79 | } 80 | } 81 | 82 | let mut result_set = Vec::new(); 83 | loop { 84 | let (packet, _) = channel.read_packet()?; 85 | check_error_packet(&packet, "Query result set error.")?; 86 | if packet[0] == ResponseType::END_OF_FILE { 87 | break; 88 | } 89 | result_set.push(ResultSetRowPacket::parse(&packet)?); 90 | } 91 | Ok(result_set) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/connect.rs: -------------------------------------------------------------------------------- 1 | use openssl::rsa::{Padding, Rsa}; 2 | 3 | use crate::binlog_client::BinlogClient; 4 | use crate::commands::auth_plugin_switch_command::AuthPluginSwitchCommand; 5 | use crate::commands::authenticate_command::AuthenticateCommand; 6 | use crate::commands::ssl_request_command::SslRequestCommand; 7 | use crate::constants::auth_plugin_names::AuthPlugin; 8 | use crate::constants::database_provider::DatabaseProvider; 9 | use crate::constants::{auth_plugin_names, capability_flags, NULL_TERMINATOR, UTF8_MB4_GENERAL_CI}; 10 | use crate::errors::Error; 11 | use crate::extensions::{check_error_packet, xor}; 12 | use crate::packet_channel::PacketChannel; 13 | use crate::responses::auth_switch_packet::AuthPluginSwitchPacket; 14 | use crate::responses::handshake_packet::HandshakePacket; 15 | use crate::responses::response_type::ResponseType; 16 | use crate::ssl_mode::SslMode; 17 | 18 | impl BinlogClient { 19 | pub fn connect(&self) -> Result<(PacketChannel, DatabaseProvider), Error> { 20 | let mut channel = PacketChannel::new(&self.options)?; 21 | let (packet, seq_num) = channel.read_packet()?; 22 | check_error_packet(&packet, "Initial handshake error.")?; 23 | let handshake = HandshakePacket::parse(&packet)?; 24 | 25 | let auth_plugin = self.get_auth_plugin(&handshake.auth_plugin_name)?; 26 | self.authenticate(&mut channel, &handshake, auth_plugin, seq_num + 1)?; 27 | Ok((channel, DatabaseProvider::from(&handshake.server_version))) 28 | } 29 | 30 | fn authenticate( 31 | &self, 32 | channel: &mut PacketChannel, 33 | handshake: &HandshakePacket, 34 | auth_plugin: AuthPlugin, 35 | mut seq_num: u8, 36 | ) -> Result<(), Error> { 37 | let mut use_ssl = false; 38 | if self.options.ssl_mode != SslMode::Disabled { 39 | let ssl_available = (handshake.server_capabilities & capability_flags::SSL) != 0; 40 | if !ssl_available && self.options.ssl_mode as u8 >= SslMode::Require as u8 { 41 | return Err(Error::String( 42 | "The server doesn't support SSL encryption".to_string(), 43 | )); 44 | } 45 | if ssl_available { 46 | let ssl_command = SslRequestCommand::new(UTF8_MB4_GENERAL_CI); 47 | channel.write_packet(&ssl_command.serialize()?, seq_num)?; 48 | seq_num += 1; 49 | channel.upgrade_to_ssl(); 50 | use_ssl = true; 51 | } 52 | } 53 | 54 | let auth_command = 55 | AuthenticateCommand::new(&self.options, handshake, auth_plugin, UTF8_MB4_GENERAL_CI); 56 | channel.write_packet(&auth_command.serialize()?, seq_num)?; 57 | let (packet, seq_num) = channel.read_packet()?; 58 | check_error_packet(&packet, "Authentication error.")?; 59 | 60 | match packet[0] { 61 | ResponseType::OK => return Ok(()), 62 | ResponseType::AUTH_PLUGIN_SWITCH => { 63 | let switch_packet = AuthPluginSwitchPacket::parse(&packet[1..])?; 64 | self.handle_auth_plugin_switch(channel, switch_packet, seq_num + 1, use_ssl)?; 65 | Ok(()) 66 | } 67 | _ => { 68 | self.authenticate_sha_256( 69 | channel, 70 | &packet, 71 | &handshake.scramble, 72 | seq_num + 1, 73 | use_ssl, 74 | )?; 75 | Ok(()) 76 | } 77 | } 78 | } 79 | 80 | fn handle_auth_plugin_switch( 81 | &self, 82 | channel: &mut PacketChannel, 83 | switch_packet: AuthPluginSwitchPacket, 84 | seq_num: u8, 85 | use_ssl: bool, 86 | ) -> Result<(), Error> { 87 | let auth_plugin = self.get_auth_plugin(&switch_packet.auth_plugin_name)?; 88 | let auth_switch_command = AuthPluginSwitchCommand::new( 89 | &self.options.password, 90 | &switch_packet.auth_plugin_data, 91 | &switch_packet.auth_plugin_name, 92 | auth_plugin, 93 | ); 94 | channel.write_packet(&auth_switch_command.serialize()?, seq_num)?; 95 | let (packet, seq_num) = channel.read_packet()?; 96 | check_error_packet(&packet, "Authentication switch error.")?; 97 | 98 | if switch_packet.auth_plugin_name == auth_plugin_names::CACHING_SHA2_PASSWORD { 99 | self.authenticate_sha_256( 100 | channel, 101 | &packet, 102 | &switch_packet.auth_plugin_data, 103 | seq_num + 1, 104 | use_ssl, 105 | )?; 106 | } 107 | Ok(()) 108 | } 109 | 110 | fn authenticate_sha_256( 111 | &self, 112 | channel: &mut PacketChannel, 113 | packet: &[u8], 114 | scramble: &String, 115 | seq_num: u8, 116 | use_ssl: bool, 117 | ) -> Result<(), Error> { 118 | // See https://mariadb.com/kb/en/caching_sha2_password-authentication-plugin/ 119 | // Success authentication. 120 | if packet[0] == 0x01 && packet[1] == 0x03 { 121 | return Ok(()); 122 | } 123 | 124 | let mut password = self.options.password.as_bytes().to_vec(); 125 | password.push(NULL_TERMINATOR); 126 | 127 | // Send clear password if ssl is used. 128 | if use_ssl { 129 | channel.write_packet(&password, seq_num)?; 130 | let (packet, _seq_num) = channel.read_packet()?; 131 | check_error_packet(&packet, "Sending clear password error.")?; 132 | return Ok(()); 133 | } 134 | 135 | // Request public key. 136 | channel.write_packet(&[0x02], seq_num)?; 137 | let (packet, seq_num) = channel.read_packet()?; 138 | check_error_packet(&packet, "Requesting caching_sha2_password public key.")?; 139 | 140 | // Extract public key. 141 | let public_key = &packet[1..]; 142 | let encrypted_password = xor(&password, &scramble.as_bytes()); 143 | 144 | let rsa = Rsa::public_key_from_pem(public_key)?; 145 | let mut encrypted_body = vec![0u8; rsa.size() as usize]; 146 | rsa.public_encrypt( 147 | &encrypted_password, 148 | &mut encrypted_body, 149 | Padding::PKCS1_OAEP, 150 | )?; 151 | 152 | channel.write_packet(&encrypted_body, seq_num + 1)?; 153 | let (packet, _seq_num) = channel.read_packet()?; 154 | check_error_packet(&packet, "Authentication error.")?; 155 | Ok(()) 156 | } 157 | 158 | fn get_auth_plugin(&self, auth_plugin_name: &String) -> Result { 159 | if auth_plugin_name == auth_plugin_names::MY_SQL_NATIVE_PASSWORD { 160 | return Ok(AuthPlugin::MySqlNativePassword); 161 | } 162 | if auth_plugin_name == auth_plugin_names::CACHING_SHA2_PASSWORD { 163 | return Ok(AuthPlugin::CachingSha2Password); 164 | } 165 | let message = format!("{} auth plugin is not supported.", auth_plugin_name); 166 | Err(Error::String(message.to_string())) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/constants/auth_plugin_names.rs: -------------------------------------------------------------------------------- 1 | /// Used by default in MariaDB and MySQL 5.7 Server and prior. 2 | pub const MY_SQL_NATIVE_PASSWORD: &str = "mysql_native_password"; 3 | 4 | /// Used by default in MySQL Server 8.0. 5 | pub const CACHING_SHA2_PASSWORD: &str = "caching_sha2_password"; 6 | 7 | pub enum AuthPlugin { 8 | MySqlNativePassword, 9 | CachingSha2Password, 10 | } 11 | -------------------------------------------------------------------------------- /src/constants/capability_flags.rs: -------------------------------------------------------------------------------- 1 | /// Server and client capability flags 2 | /// See more 3 | 4 | pub const LONG_PASSWORD: u64 = 1 << 0; 5 | pub const FOUND_ROWS: u64 = 1 << 1; 6 | pub const LONG_FLAG: u64 = 1 << 2; 7 | pub const CONNECT_WITH_DB: u64 = 1 << 3; 8 | pub const NO_SCHEMA: u64 = 1 << 4; 9 | pub const COMPRESS: u64 = 1 << 5; 10 | pub const ODBC: u64 = 1 << 6; 11 | pub const LOCAL_FILES: u64 = 1 << 7; 12 | pub const IGNORE_SPACE: u64 = 1 << 8; 13 | pub const PROTOCOL_41: u64 = 1 << 9; 14 | pub const INTERACTIVE: u64 = 1 << 10; 15 | pub const SSL: u64 = 1 << 11; 16 | pub const IGNORE_SIGPIPE: u64 = 1 << 12; 17 | pub const TRANSACTIONS: u64 = 1 << 13; 18 | pub const RESERVED: u64 = 1 << 14; 19 | pub const SECURE_CONNECTION: u64 = 1 << 15; 20 | pub const MULTI_STATEMENTS: u64 = 1 << 16; 21 | pub const MULTI_RESULTS: u64 = 1 << 17; 22 | pub const PS_MULTI_RESULTS: u64 = 1 << 18; 23 | pub const PLUGIN_AUTH: u64 = 1 << 19; 24 | -------------------------------------------------------------------------------- /src/constants/checksum_type.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | 3 | /// Checksum type used in a binlog file. 4 | #[derive(Clone, Copy, Debug)] 5 | pub enum ChecksumType { 6 | /// Checksum is disabled. 7 | None = 0, 8 | 9 | /// CRC32 checksum. 10 | Crc32 = 1, 11 | } 12 | 13 | impl ChecksumType { 14 | pub fn from_code(code: u8) -> Result { 15 | match code { 16 | 0 => Ok(ChecksumType::None), 17 | 1 => Ok(ChecksumType::Crc32), 18 | _ => Err(Error::String( 19 | format!("The master checksum type is not supported: {}", code).to_string(), 20 | )), 21 | } 22 | } 23 | 24 | pub fn from_name(name: &str) -> Result { 25 | match name { 26 | "NONE" => Ok(ChecksumType::None), 27 | "CRC32" => Ok(ChecksumType::Crc32), 28 | _ => Err(Error::String( 29 | format!("The master checksum type is not supported: {}", name).to_string(), 30 | )), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/constants/column_type.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | 3 | /// MySql column types. 4 | /// See MariaDB docs 5 | /// See MySQL docs 6 | #[derive(PartialEq, Debug)] 7 | pub enum ColumnType { 8 | /// DECIMAL 9 | Decimal = 0, 10 | 11 | /// TINY 12 | Tiny = 1, 13 | 14 | /// SHORT 15 | Short = 2, 16 | 17 | /// LONG 18 | Long = 3, 19 | 20 | /// FLOAT 21 | Float = 4, 22 | 23 | /// DOUBLE 24 | Double = 5, 25 | 26 | /// NULL 27 | Null = 6, 28 | 29 | /// TIMESTAMP 30 | TimeStamp = 7, 31 | 32 | /// LONGLONG 33 | LongLong = 8, 34 | 35 | /// INT24 36 | Int24 = 9, 37 | 38 | /// DATE 39 | Date = 10, 40 | 41 | /// TIME 42 | Time = 11, 43 | 44 | /// DATETIME 45 | DateTime = 12, 46 | 47 | /// YEAR 48 | Year = 13, 49 | 50 | /// NEWDATE 51 | NewDate = 14, 52 | 53 | /// VARCHAR 54 | VarChar = 15, 55 | 56 | /// BIT 57 | Bit = 16, 58 | 59 | /// TIMESTAMP2 60 | TimeStamp2 = 17, 61 | 62 | /// DATETIME2 63 | DateTime2 = 18, 64 | 65 | /// TIME2 66 | Time2 = 19, 67 | 68 | /// JSON is MySQL 5.7.8+ type. Not supported in MariaDB. 69 | Json = 245, 70 | 71 | /// NEWDECIMAL 72 | NewDecimal = 246, 73 | 74 | /// ENUM 75 | Enum = 247, 76 | 77 | /// SET 78 | Set = 248, 79 | 80 | /// TINY_BLOB 81 | TinyBlob = 249, 82 | 83 | /// MEDIUM_BLOB 84 | MediumBlob = 250, 85 | 86 | /// LONG_BLOB 87 | LongBlob = 251, 88 | 89 | /// BLOB 90 | Blob = 252, 91 | 92 | /// VAR_STRING 93 | VarString = 253, 94 | 95 | /// STRING 96 | String = 254, 97 | 98 | /// GEOMETRY 99 | Geometry = 255, 100 | } 101 | 102 | impl ColumnType { 103 | pub fn from_code(code: u8) -> Result { 104 | let value = match code { 105 | 0 => ColumnType::Decimal, 106 | 1 => ColumnType::Tiny, 107 | 2 => ColumnType::Short, 108 | 3 => ColumnType::Long, 109 | 4 => ColumnType::Float, 110 | 5 => ColumnType::Double, 111 | 6 => ColumnType::Null, 112 | 7 => ColumnType::TimeStamp, 113 | 8 => ColumnType::LongLong, 114 | 9 => ColumnType::Int24, 115 | 10 => ColumnType::Date, 116 | 11 => ColumnType::Time, 117 | 12 => ColumnType::DateTime, 118 | 13 => ColumnType::Year, 119 | 14 => ColumnType::NewDate, 120 | 15 => ColumnType::VarChar, 121 | 16 => ColumnType::Bit, 122 | 17 => ColumnType::TimeStamp2, 123 | 18 => ColumnType::DateTime2, 124 | 19 => ColumnType::Time2, 125 | 245 => ColumnType::Json, 126 | 246 => ColumnType::NewDecimal, 127 | 247 => ColumnType::Enum, 128 | 248 => ColumnType::Set, 129 | 249 => ColumnType::TinyBlob, 130 | 250 => ColumnType::MediumBlob, 131 | 251 => ColumnType::LongBlob, 132 | 252 => ColumnType::Blob, 133 | 253 => ColumnType::VarString, 134 | 254 => ColumnType::String, 135 | 255 => ColumnType::Geometry, 136 | _ => return Err(Error::String(format!("Unknown column type {}", code))), 137 | }; 138 | Ok(value) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/constants/database_provider.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum DatabaseProvider { 3 | MariaDB, 4 | MySQL, 5 | } 6 | 7 | impl DatabaseProvider { 8 | pub fn from(server: &String) -> Self { 9 | match server.contains("MariaDB") { 10 | true => DatabaseProvider::MariaDB, 11 | _ => DatabaseProvider::MySQL, 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/constants/mod.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | pub mod auth_plugin_names; 4 | pub mod capability_flags; 5 | pub mod checksum_type; 6 | pub mod column_type; 7 | pub mod database_provider; 8 | 9 | ///Packet Constants 10 | pub const PACKET_HEADER_SIZE: usize = 4; 11 | pub const MAX_BODY_LENGTH: usize = 16777215; 12 | pub const NULL_TERMINATOR: u8 = 0; 13 | pub const UTF8_MB4_GENERAL_CI: u8 = 45; 14 | 15 | ///Event Constants 16 | pub const EVENT_HEADER_SIZE: usize = 19; 17 | pub const PAYLOAD_BUFFER_SIZE: usize = 32 * 1024; 18 | pub const FIRST_EVENT_POSITION: usize = 4; 19 | 20 | /// Timeout constants 21 | /// Takes into account network latency. 22 | pub const TIMEOUT_LATENCY_DELTA: Duration = Duration::from_secs(10); 23 | pub const TIMEOUT_MESSAGE: &str = 24 | "Could not receive a master heartbeat within the specified interval"; 25 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::{io, num::ParseIntError, str::Utf8Error, string::FromUtf8Error}; 2 | 3 | use hex::FromHexError; 4 | use openssl::error::ErrorStack; 5 | 6 | #[derive(Debug)] 7 | pub enum Error { 8 | IoError(io::Error), 9 | Utf8Error(Utf8Error), 10 | FromUtf8Error(FromUtf8Error), 11 | FromHexError(FromHexError), 12 | ParseIntError(ParseIntError), 13 | ErrorStack(ErrorStack), 14 | String(String), 15 | } 16 | 17 | impl From for Error { 18 | fn from(error: io::Error) -> Self { 19 | Error::IoError(error) 20 | } 21 | } 22 | 23 | impl From for Error { 24 | fn from(error: Utf8Error) -> Self { 25 | Error::Utf8Error(error) 26 | } 27 | } 28 | 29 | impl From for Error { 30 | fn from(error: FromUtf8Error) -> Self { 31 | Error::FromUtf8Error(error) 32 | } 33 | } 34 | 35 | impl From for Error { 36 | fn from(error: FromHexError) -> Self { 37 | Error::FromHexError(error) 38 | } 39 | } 40 | 41 | impl From for Error { 42 | fn from(error: ParseIntError) -> Self { 43 | Error::ParseIntError(error) 44 | } 45 | } 46 | 47 | impl From for Error { 48 | fn from(error: ErrorStack) -> Self { 49 | Error::ErrorStack(error) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/events/binlog_event.rs: -------------------------------------------------------------------------------- 1 | use crate::events::format_description_event::FormatDescriptionEvent; 2 | use crate::events::heartbeat_event::HeartbeatEvent; 3 | use crate::events::intvar_event::IntVarEvent; 4 | use crate::events::query_event::QueryEvent; 5 | use crate::events::rotate_event::RotateEvent; 6 | use crate::events::row_events::delete_rows_event::DeleteRowsEvent; 7 | use crate::events::row_events::update_rows_event::UpdateRowsEvent; 8 | use crate::events::row_events::write_rows_event::WriteRowsEvent; 9 | use crate::events::rows_query_event::RowsQueryEvent; 10 | use crate::events::table_map_event::TableMapEvent; 11 | use crate::events::uservar_event::UserVarEvent; 12 | use crate::events::xid_event::XidEvent; 13 | use crate::providers::mariadb::events::gtid_event::GtidEvent as MariaDbGtidEvent; 14 | use crate::providers::mariadb::events::gtid_list_event::GtidListEvent; 15 | use crate::providers::mysql::events::gtid_event::GtidEvent as MySqlGtidEvent; 16 | use crate::providers::mysql::events::prev_gtids_event::PreviousGtidsEvent; 17 | 18 | /// Represents a binlog event. 19 | #[derive(Debug)] 20 | pub enum BinlogEvent { 21 | UnknownEvent, 22 | DeleteRowsEvent(DeleteRowsEvent), 23 | UpdateRowsEvent(UpdateRowsEvent), 24 | WriteRowsEvent(WriteRowsEvent), 25 | XidEvent(XidEvent), 26 | IntVarEvent(IntVarEvent), 27 | UserVarEvent(UserVarEvent), 28 | QueryEvent(QueryEvent), 29 | TableMapEvent(TableMapEvent), 30 | RotateEvent(RotateEvent), 31 | RowsQueryEvent(RowsQueryEvent), 32 | HeartbeatEvent(HeartbeatEvent), 33 | FormatDescriptionEvent(FormatDescriptionEvent), 34 | // Provider specific events 35 | MySqlGtidEvent(MySqlGtidEvent), 36 | MySqlPrevGtidsEvent(PreviousGtidsEvent), 37 | MariaDbGtidEvent(MariaDbGtidEvent), 38 | MariaDbGtidListEvent(GtidListEvent), 39 | } 40 | -------------------------------------------------------------------------------- /src/events/event_header.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, ReadBytesExt}; 2 | use std::io::Cursor; 3 | 4 | use crate::errors::Error; 5 | 6 | /// Binlog event header version 4. Header size is 19 bytes. 7 | /// See MariaDB docs 8 | /// See MySQL docs 9 | #[derive(Debug)] 10 | pub struct EventHeader { 11 | /// Provides creation time in seconds from Unix. 12 | pub timestamp: u32, 13 | 14 | /// Gets type of the binlog event. 15 | pub event_type: u8, 16 | 17 | /// Gets id of the server that created the event. 18 | pub server_id: u32, 19 | 20 | /// Gets event length (header + event + checksum). 21 | pub event_length: u32, 22 | 23 | /// Gets file position of next event. 24 | pub next_event_position: u32, 25 | 26 | /// Gets event flags. 27 | /// See documentation. 28 | pub event_flags: u16, 29 | } 30 | 31 | impl EventHeader { 32 | pub fn parse(slice: &[u8]) -> Result { 33 | let mut cursor = Cursor::new(slice); 34 | Ok(Self { 35 | timestamp: cursor.read_u32::()?, 36 | event_type: cursor.read_u8()?, 37 | server_id: cursor.read_u32::()?, 38 | event_length: cursor.read_u32::()?, 39 | next_event_position: cursor.read_u32::()?, 40 | event_flags: cursor.read_u16::()?, 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/events/event_parser.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::checksum_type::ChecksumType; 2 | use crate::errors::Error; 3 | use crate::events::binlog_event::BinlogEvent; 4 | use crate::events::event_header::EventHeader; 5 | use crate::events::event_type::EventType; 6 | use crate::events::format_description_event::FormatDescriptionEvent; 7 | use crate::events::heartbeat_event::HeartbeatEvent; 8 | use crate::events::intvar_event::IntVarEvent; 9 | use crate::events::query_event::QueryEvent; 10 | use crate::events::rotate_event::RotateEvent; 11 | use crate::events::row_events::delete_rows_event::DeleteRowsEvent; 12 | use crate::events::row_events::update_rows_event::UpdateRowsEvent; 13 | use crate::events::row_events::write_rows_event::WriteRowsEvent; 14 | use crate::events::rows_query_event::RowsQueryEvent; 15 | use crate::events::table_map_event::TableMapEvent; 16 | use crate::events::uservar_event::UserVarEvent; 17 | use crate::events::xid_event::XidEvent; 18 | use crate::providers::mariadb::events::gtid_event::GtidEvent as MariaDbGtidEvent; 19 | use crate::providers::mariadb::events::gtid_list_event::GtidListEvent; 20 | use crate::providers::mysql::events::gtid_event::GtidEvent as MySqlGtidEvent; 21 | use crate::providers::mysql::events::prev_gtids_event::PreviousGtidsEvent; 22 | use std::collections::HashMap; 23 | use std::io::Cursor; 24 | 25 | pub struct EventParser { 26 | /// Gets checksum algorithm type used in a binlog file. 27 | pub checksum_type: ChecksumType, 28 | 29 | /// Gets TableMapEvent cache required in row events. 30 | table_map: HashMap, 31 | } 32 | 33 | impl EventParser { 34 | pub fn new() -> Self { 35 | Self { 36 | checksum_type: ChecksumType::None, 37 | table_map: HashMap::new(), 38 | } 39 | } 40 | 41 | pub fn parse_event( 42 | &mut self, 43 | header: &EventHeader, 44 | slice: &[u8], 45 | ) -> Result { 46 | // Consider verifying checksum 47 | let mut cursor = match self.checksum_type { 48 | ChecksumType::None => Cursor::new(slice), 49 | ChecksumType::Crc32 => Cursor::new(&slice[0..slice.len() - 4]), 50 | }; 51 | 52 | let binlog_event: BinlogEvent = match EventType::from_code(header.event_type) { 53 | EventType::FormatDescriptionEvent => BinlogEvent::FormatDescriptionEvent( 54 | FormatDescriptionEvent::parse(&mut cursor, &header)?, 55 | ), 56 | EventType::TableMapEvent => { 57 | BinlogEvent::TableMapEvent(TableMapEvent::parse(&mut cursor)?) 58 | } 59 | EventType::HeartbeatEvent => { 60 | BinlogEvent::HeartbeatEvent(HeartbeatEvent::parse(&mut cursor)?) 61 | } 62 | EventType::RotateEvent => BinlogEvent::RotateEvent(RotateEvent::parse(&mut cursor)?), 63 | EventType::IntvarEvent => BinlogEvent::IntVarEvent(IntVarEvent::parse(&mut cursor)?), 64 | EventType::UserVarEvent => BinlogEvent::UserVarEvent(UserVarEvent::parse(&mut cursor)?), 65 | EventType::QueryEvent => BinlogEvent::QueryEvent(QueryEvent::parse(&mut cursor)?), 66 | EventType::XidEvent => BinlogEvent::XidEvent(XidEvent::parse(&mut cursor)?), 67 | // Rows events used in MariaDB and MySQL from 5.1.15 to 5.6. 68 | EventType::WriteRowsEventV1 => { 69 | BinlogEvent::WriteRowsEvent(WriteRowsEvent::parse(&mut cursor, &self.table_map, 1)?) 70 | } 71 | EventType::UpdateRowsEventV1 => BinlogEvent::UpdateRowsEvent(UpdateRowsEvent::parse( 72 | &mut cursor, 73 | &self.table_map, 74 | 1, 75 | )?), 76 | EventType::DeleteRowsEventV1 => BinlogEvent::DeleteRowsEvent(DeleteRowsEvent::parse( 77 | &mut cursor, 78 | &self.table_map, 79 | 1, 80 | )?), 81 | // MySQL specific events. Rows events used only in MySQL from 5.6 to 8.0. 82 | EventType::MySqlWriteRowsEventV2 => { 83 | BinlogEvent::WriteRowsEvent(WriteRowsEvent::parse(&mut cursor, &self.table_map, 2)?) 84 | } 85 | EventType::MySqlUpdateRowsEventV2 => BinlogEvent::UpdateRowsEvent( 86 | UpdateRowsEvent::parse(&mut cursor, &self.table_map, 2)?, 87 | ), 88 | EventType::MySqlDeleteRowsEventV2 => BinlogEvent::DeleteRowsEvent( 89 | DeleteRowsEvent::parse(&mut cursor, &self.table_map, 2)?, 90 | ), 91 | EventType::MySqlRowsQueryEvent => { 92 | BinlogEvent::RowsQueryEvent(RowsQueryEvent::parse_mysql(&mut cursor)?) 93 | } 94 | EventType::MySqlGtidEvent => { 95 | BinlogEvent::MySqlGtidEvent(MySqlGtidEvent::parse(&mut cursor)?) 96 | } 97 | EventType::MySqlPreviousGtidsEvent => { 98 | BinlogEvent::MySqlPrevGtidsEvent(PreviousGtidsEvent::parse(&mut cursor)?) 99 | } 100 | // MariaDB specific events 101 | EventType::MariaDbGtidEvent => { 102 | BinlogEvent::MariaDbGtidEvent(MariaDbGtidEvent::parse(&mut cursor, &header)?) 103 | } 104 | EventType::MariaDbGtidListEvent => { 105 | BinlogEvent::MariaDbGtidListEvent(GtidListEvent::parse(&mut cursor)?) 106 | } 107 | EventType::MariaDbAnnotateRowsEvent => { 108 | BinlogEvent::RowsQueryEvent(RowsQueryEvent::parse_mariadb(&mut cursor)?) 109 | } 110 | _ => BinlogEvent::UnknownEvent, 111 | }; 112 | 113 | if let BinlogEvent::FormatDescriptionEvent(x) = &binlog_event { 114 | self.checksum_type = x.checksum_type; 115 | } 116 | 117 | if let BinlogEvent::TableMapEvent(x) = &binlog_event { 118 | self.table_map.insert(x.table_id, x.clone()); //todo: optimize 119 | } 120 | 121 | Ok(binlog_event) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/events/event_type.rs: -------------------------------------------------------------------------------- 1 | /// Binlog event types. 2 | /// See event header docs 3 | /// See list of event types 4 | /// See rows event docs 5 | pub enum EventType { 6 | Unknown, 7 | 8 | /// Identifies . 9 | QueryEvent = 2, 10 | 11 | /// Identifies StopEvent. 12 | StopEvent = 3, 13 | 14 | /// Identifies . 15 | RotateEvent = 4, 16 | 17 | /// Identifies . 18 | IntvarEvent = 5, 19 | 20 | /// Identifies RandEvent. 21 | RandEvent = 13, 22 | 23 | /// Identifies UserVarEvent. 24 | UserVarEvent = 14, 25 | 26 | /// Identifies . 27 | FormatDescriptionEvent = 15, 28 | 29 | /// Identifies . 30 | XidEvent = 16, 31 | 32 | /// Identifies . 33 | TableMapEvent = 19, 34 | 35 | /// Row events 36 | /// Identifies in MariaDB and MySQL from 5.1.15 to 5.6. 37 | WriteRowsEventV1 = 23, 38 | 39 | /// Identifies in MariaDB and MySQL from 5.1.15 to 5.6. 40 | UpdateRowsEventV1 = 24, 41 | 42 | /// Identifies in MariaDB and MySQL from 5.1.15 to 5.6. 43 | DeleteRowsEventV1 = 25, 44 | 45 | /// Identifies . 46 | HeartbeatEvent = 27, 47 | 48 | /// MySQL specific events 49 | /// Identifies in MySQL from 5.6 to 8.0. 50 | MySqlRowsQueryEvent = 29, 51 | 52 | /// Identifies in MySQL from 5.6 to 8.0. 53 | MySqlWriteRowsEventV2 = 30, 54 | 55 | /// Identifies in MySQL from 5.6 to 8.0. 56 | MySqlUpdateRowsEventV2 = 31, 57 | 58 | /// Identifies in MySQL from 5.6 to 8.0. 59 | MySqlDeleteRowsEventV2 = 32, 60 | 61 | /// Identifies in MySQL from 5.6 to 8.0. 62 | MySqlGtidEvent = 33, 63 | 64 | /// Identifies in MySQL from 5.6 to 8.0. 65 | MySqlPreviousGtidsEvent = 35, 66 | 67 | /// Identifies in MySQL from 5.6 to 8.0. 68 | MySqlXaPrepare = 38, 69 | 70 | /// MariaDB specific events 71 | /// Identifies in MariaDB. 72 | MariaDbAnnotateRowsEvent = 160, 73 | 74 | /// Identifies binlog checkpoint event in MariaDB. 75 | MariaDbBinlogCheckpointEvent = 161, 76 | 77 | /// Identifies in MariaDB. 78 | MariaDbGtidEvent = 162, 79 | 80 | /// Identifies in MariaDB. 81 | MariaDbGtidListEvent = 163, 82 | 83 | /// Identifies encryption start event in MariaDB. 84 | MariaDbStartEncryptionEvent = 164, 85 | } 86 | 87 | impl EventType { 88 | pub fn from_code(code: u8) -> Self { 89 | match code { 90 | 2 => EventType::QueryEvent, 91 | 3 => EventType::StopEvent, 92 | 4 => EventType::RotateEvent, 93 | 5 => EventType::IntvarEvent, 94 | 13 => EventType::RandEvent, 95 | 14 => EventType::UserVarEvent, 96 | 15 => EventType::FormatDescriptionEvent, 97 | 16 => EventType::XidEvent, 98 | 19 => EventType::TableMapEvent, 99 | 23 => EventType::WriteRowsEventV1, 100 | 24 => EventType::UpdateRowsEventV1, 101 | 25 => EventType::DeleteRowsEventV1, 102 | 27 => EventType::HeartbeatEvent, 103 | 29 => EventType::MySqlRowsQueryEvent, 104 | 30 => EventType::MySqlWriteRowsEventV2, 105 | 31 => EventType::MySqlUpdateRowsEventV2, 106 | 32 => EventType::MySqlDeleteRowsEventV2, 107 | 33 => EventType::MySqlGtidEvent, 108 | 35 => EventType::MySqlPreviousGtidsEvent, 109 | 38 => EventType::MySqlXaPrepare, 110 | 160 => EventType::MariaDbAnnotateRowsEvent, 111 | 161 => EventType::MariaDbBinlogCheckpointEvent, 112 | 162 => EventType::MariaDbGtidEvent, 113 | 163 => EventType::MariaDbGtidListEvent, 114 | 164 => EventType::MariaDbStartEncryptionEvent, 115 | _ => EventType::Unknown, 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/events/format_description_event.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::checksum_type::ChecksumType; 2 | use crate::events::event_header::EventHeader; 3 | use crate::events::event_type::EventType; 4 | use crate::{constants, errors::Error}; 5 | use byteorder::{LittleEndian, ReadBytesExt}; 6 | use std::io::{Cursor, Read, Seek, SeekFrom}; 7 | 8 | const EVENT_TYPES_OFFSET: u8 = 2 + 50 + 4 + 1; 9 | 10 | /// Written as the first event in binlog file or when replication is started. 11 | /// See MariaDB docs 12 | /// See MySQL docs 13 | /// See start events flow 14 | #[derive(Debug)] 15 | pub struct FormatDescriptionEvent { 16 | /// Gets binary log format version. This should always be 4. 17 | pub binlog_version: u16, 18 | 19 | /// Gets MariaDB/MySQL server version name. 20 | pub server_version: String, 21 | 22 | /// Gets checksum algorithm type. 23 | pub checksum_type: ChecksumType, 24 | } 25 | 26 | impl FormatDescriptionEvent { 27 | /// Supports all versions of MariaDB and MySQL 5.0+ (V4 header format). 28 | pub fn parse(cursor: &mut Cursor<&[u8]>, header: &EventHeader) -> Result { 29 | let binlog_version = cursor.read_u16::()?; 30 | 31 | // Read server version 32 | let mut server_version = [0u8; 50]; 33 | cursor.read_exact(&mut server_version)?; 34 | let mut slice: &[u8] = &server_version; 35 | if let Some(zero_index) = server_version.iter().position(|&b| b == 0) { 36 | slice = &server_version[..zero_index]; 37 | } 38 | let server_version = std::str::from_utf8(slice)?.to_string(); 39 | 40 | // Redundant timestamp & header length which is always 19 41 | cursor.seek(SeekFrom::Current(5))?; 42 | 43 | // Get size of the event payload to determine beginning of the checksum part 44 | let seek_len = EventType::FormatDescriptionEvent as i64 - 1; 45 | cursor.seek(SeekFrom::Current(seek_len))?; 46 | let payload_length = cursor.read_u8()?; 47 | 48 | let mut checksum_type = ChecksumType::None; 49 | if payload_length != header.event_length as u8 - constants::EVENT_HEADER_SIZE as u8 { 50 | let skip = payload_length as i64 51 | - EVENT_TYPES_OFFSET as i64 52 | - EventType::FormatDescriptionEvent as i64; 53 | 54 | cursor.seek(SeekFrom::Current(skip))?; 55 | checksum_type = ChecksumType::from_code(cursor.read_u8()?)?; 56 | } 57 | 58 | Ok(Self { 59 | binlog_version, 60 | server_version, 61 | checksum_type, 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/events/heartbeat_event.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Cursor, Read}; 2 | 3 | use crate::errors::Error; 4 | 5 | /// The event is sent from master to the client for keep alive feature. 6 | /// See more 7 | #[derive(Debug)] 8 | pub struct HeartbeatEvent { 9 | /// Gets current master binlog filename 10 | pub binlog_filename: String, 11 | } 12 | 13 | impl HeartbeatEvent { 14 | /// Supports all versions of MariaDB and MySQL. 15 | pub fn parse(cursor: &mut Cursor<&[u8]>) -> Result { 16 | let mut binlog_filename = String::new(); 17 | cursor.read_to_string(&mut binlog_filename)?; 18 | 19 | Ok(Self { binlog_filename }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/events/intvar_event.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, ReadBytesExt}; 2 | use std::io::Cursor; 3 | 4 | use crate::errors::Error; 5 | 6 | /// Generated when an auto increment column or LAST_INSERT_ID() function are used. 7 | /// See more 8 | #[derive(Debug)] 9 | pub struct IntVarEvent { 10 | /// Gets type. 11 | /// 0x00 - Invalid value. 12 | /// 0x01 - LAST_INSERT_ID. 13 | /// 0x02 - Insert id (auto_increment). 14 | pub intvar_type: u8, 15 | 16 | /// Gets value. 17 | pub value: u64, 18 | } 19 | 20 | impl IntVarEvent { 21 | /// Supports all versions of MariaDB and MySQL. 22 | pub fn parse(cursor: &mut Cursor<&[u8]>) -> Result { 23 | let intvar_type = cursor.read_u8()?; 24 | let value = cursor.read_u64::()?; 25 | 26 | Ok(Self { intvar_type, value }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/events/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod binlog_event; 2 | pub mod event_header; 3 | pub mod event_parser; 4 | pub mod event_type; 5 | pub mod format_description_event; 6 | pub mod heartbeat_event; 7 | pub mod intvar_event; 8 | pub mod query_event; 9 | pub mod rotate_event; 10 | pub mod rows_query_event; 11 | pub mod table_map_event; 12 | pub mod uservar_event; 13 | pub mod xid_event; 14 | 15 | pub mod row_events; 16 | -------------------------------------------------------------------------------- /src/events/query_event.rs: -------------------------------------------------------------------------------- 1 | use crate::{errors::Error, extensions::read_string}; 2 | use byteorder::{LittleEndian, ReadBytesExt}; 3 | use std::io::{Cursor, Read, Seek, SeekFrom}; 4 | 5 | /// Represents sql statement in binary log. 6 | /// See more 7 | #[derive(Debug)] 8 | pub struct QueryEvent { 9 | /// Gets id of the thread that issued the statement. 10 | pub thread_id: u32, 11 | 12 | /// Gets the execution time of the statement in seconds. 13 | pub duration: u32, 14 | 15 | /// Gets the error code of the executed statement. 16 | pub error_code: u16, 17 | 18 | /// Gets status variables. 19 | pub status_variables: Vec, 20 | 21 | /// Gets the default database name. 22 | pub database_name: String, 23 | 24 | /// Gets the SQL statement. 25 | pub sql_statement: String, 26 | } 27 | 28 | impl QueryEvent { 29 | /// Supports all versions of MariaDB and MySQL. 30 | pub fn parse(cursor: &mut Cursor<&[u8]>) -> Result { 31 | let thread_id = cursor.read_u32::()?; 32 | let duration = cursor.read_u32::()?; 33 | 34 | // DatabaseName length 35 | let database_name_length = cursor.read_u8()?; 36 | 37 | let error_code = cursor.read_u16::()?; 38 | let status_variable_length = cursor.read_u16::()?; 39 | 40 | let mut status_variables: Vec = vec![0; status_variable_length as usize]; 41 | cursor.read_exact(&mut status_variables[0..status_variable_length as usize])?; 42 | 43 | // DatabaseName is null terminated 44 | let database_name = read_string(cursor, database_name_length as usize)?; 45 | cursor.seek(SeekFrom::Current(1))?; 46 | 47 | let mut sql_statement = String::new(); 48 | cursor.read_to_string(&mut sql_statement)?; 49 | 50 | Ok(Self { 51 | thread_id, 52 | duration, 53 | error_code, 54 | status_variables, 55 | database_name, 56 | sql_statement, 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/events/rotate_event.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, ReadBytesExt}; 2 | use std::io::{Cursor, Read}; 3 | 4 | use crate::errors::Error; 5 | 6 | /// Last event in a binlog file which points to next binlog file. 7 | /// Fake version is also returned when replication is started. 8 | /// See more 9 | #[derive(Debug)] 10 | pub struct RotateEvent { 11 | /// Gets next binlog filename 12 | pub binlog_filename: String, 13 | 14 | /// Gets next binlog position 15 | pub binlog_position: u64, 16 | } 17 | 18 | impl RotateEvent { 19 | /// Supports all versions of MariaDB and MySQL. 20 | pub fn parse(cursor: &mut Cursor<&[u8]>) -> Result { 21 | let binlog_position = cursor.read_u64::()?; 22 | 23 | let mut binlog_filename = String::new(); 24 | cursor.read_to_string(&mut binlog_filename)?; 25 | 26 | Ok(Self { 27 | binlog_position, 28 | binlog_filename, 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/events/row_events/actual_string_type.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::column_type::ColumnType; 2 | 3 | /// Parses actual string type 4 | /// See: https://bugs.mysql.com/bug.php?id=37426 5 | /// See: https://github.com/mysql/mysql-server/blob/9c3a49ec84b521cb0b35383f119099b2eb25d4ff/sql/log_event.cc#L1988 6 | pub fn get_actual_string_type(column_type: &mut u8, metadata: &mut u16) { 7 | // CHAR column type 8 | if *metadata < 256 { 9 | return; 10 | } 11 | 12 | // CHAR or ENUM or SET column types 13 | let byte0 = (*metadata >> 8) as u8; 14 | let byte1 = *metadata & 0xFF; 15 | 16 | if (byte0 & 0x30) != 0x30 { 17 | /* a long CHAR() field: see #37426 */ 18 | *metadata = byte1 | (((byte0 as u16 & 0x30) ^ 0x30) << 4); 19 | *column_type = byte0 | 0x30; 20 | } else { 21 | if byte0 == ColumnType::Enum as u8 || byte0 == ColumnType::Set as u8 { 22 | *column_type = byte0; 23 | } 24 | *metadata = byte1; 25 | } 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::get_actual_string_type; 31 | use crate::constants::column_type::ColumnType; 32 | 33 | #[test] 34 | fn get_actual_string_type_char() { 35 | // char(200) 36 | let mut column_type = ColumnType::String as u8; 37 | let mut metadata: u16 = 52768; 38 | get_actual_string_type(&mut column_type, &mut metadata); 39 | 40 | assert_eq!(ColumnType::String as u8, column_type); 41 | assert_eq!(800 /* 200*Utf8Mb4 */, metadata); 42 | } 43 | 44 | #[test] 45 | fn get_actual_string_type_enum() { 46 | // enum('Low', 'Medium', 'High') 47 | let mut column_type = ColumnType::String as u8; 48 | let mut metadata: u16 = 63233; 49 | get_actual_string_type(&mut column_type, &mut metadata); 50 | 51 | assert_eq!(ColumnType::Enum as u8, column_type); 52 | assert_eq!(1, metadata); 53 | } 54 | 55 | #[test] 56 | fn get_actual_string_type_set() { 57 | // set('Green', 'Yellow', 'Red') 58 | let mut column_type = ColumnType::String as u8; 59 | let mut metadata: u16 = 63489; 60 | get_actual_string_type(&mut column_type, &mut metadata); 61 | 62 | assert_eq!(ColumnType::Set as u8, column_type); 63 | assert_eq!(1, metadata); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/events/row_events/col_parser.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | use crate::events::row_events::mysql_value::{Date, DateTime, Time}; 3 | use crate::extensions::{read_bitmap_big_endian, read_string}; 4 | use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; 5 | use std::io::{Cursor, Read}; 6 | 7 | pub fn parse_string(cursor: &mut Cursor<&[u8]>, metadata: u16) -> Result { 8 | let length = if metadata < 256 { 9 | cursor.read_u8()? as usize 10 | } else { 11 | cursor.read_u16::()? as usize 12 | }; 13 | Ok(read_string(cursor, length)?) 14 | } 15 | 16 | pub fn parse_bit(cursor: &mut Cursor<&[u8]>, metadata: u16) -> Result, Error> { 17 | let length = (metadata >> 8) * 8 + (metadata & 0xFF); 18 | let mut bitmap = read_bitmap_big_endian(cursor, length as usize)?; 19 | bitmap.reverse(); 20 | Ok(bitmap) 21 | } 22 | 23 | pub fn parse_blob(cursor: &mut Cursor<&[u8]>, metadata: u16) -> Result, Error> { 24 | let length = cursor.read_uint::(metadata as usize)? as usize; 25 | let mut vec = vec![0; length]; 26 | cursor.read_exact(&mut vec)?; 27 | Ok(vec) 28 | } 29 | 30 | pub fn parse_year(cursor: &mut Cursor<&[u8]>, _metadata: u16) -> Result { 31 | Ok(1900 + cursor.read_u8()? as u16) 32 | } 33 | 34 | pub fn parse_date(cursor: &mut Cursor<&[u8]>, _metadata: u16) -> Result { 35 | let value = cursor.read_u24::()?; 36 | 37 | // Bits 1-5 store the day. Bits 6-9 store the month. The remaining bits store the year. 38 | let day = value % (1 << 5); 39 | let month = (value >> 5) % (1 << 4); 40 | let year = value >> 9; 41 | 42 | Ok(Date { 43 | year: year as u16, 44 | month: month as u8, 45 | day: day as u8, 46 | }) 47 | } 48 | 49 | pub fn parse_time(cursor: &mut Cursor<&[u8]>, _metadata: u16) -> Result { 50 | let mut value = (cursor.read_i24::()? << 8) >> 8; 51 | 52 | if value < 0 { 53 | return Err(Error::String( 54 | "Parsing negative TIME values is not supported in this version".to_string(), 55 | )); 56 | } 57 | 58 | let second = value % 100; 59 | value = value / 100; 60 | let minute = value % 100; 61 | value = value / 100; 62 | let hour = value; 63 | Ok(Time { 64 | hour: hour as i16, 65 | minute: minute as u8, 66 | second: second as u8, 67 | millis: 0, 68 | }) 69 | } 70 | 71 | pub fn parse_time2(cursor: &mut Cursor<&[u8]>, metadata: u16) -> Result { 72 | let value = cursor.read_u24::()?; 73 | let millis = parse_fractional_part(cursor, metadata)? / 1000; 74 | 75 | let negative = ((value >> 23) & 1) == 0; 76 | if negative { 77 | // It looks like other similar clients don't parse TIME2 values properly 78 | // In negative time values both TIME and FSP are stored in reverse order 79 | // See https://github.com/mysql/mysql-server/blob/ea7d2e2d16ac03afdd9cb72a972a95981107bf51/sql/log_event.cc#L2022 80 | // See https://github.com/mysql/mysql-server/blob/ea7d2e2d16ac03afdd9cb72a972a95981107bf51/mysys/my_time.cc#L1784 81 | return Err(Error::String( 82 | "Parsing negative TIME values is not supported in this version".to_string(), 83 | )); 84 | } 85 | 86 | // 1 bit sign. 1 bit unused. 10 bits hour. 6 bits minute. 6 bits second. 87 | let hour = (value >> 12) % (1 << 10); 88 | let minute = (value >> 6) % (1 << 6); 89 | let second = value % (1 << 6); 90 | 91 | Ok(Time { 92 | hour: hour as i16, 93 | minute: minute as u8, 94 | second: second as u8, 95 | millis: millis as u32, 96 | }) 97 | } 98 | 99 | pub fn parse_date_time(cursor: &mut Cursor<&[u8]>, _metadata: u16) -> Result { 100 | let mut value = cursor.read_u64::()?; 101 | let second = value % 100; 102 | value = value / 100; 103 | let minute = value % 100; 104 | value = value / 100; 105 | let hour = value % 100; 106 | value = value / 100; 107 | let day = value % 100; 108 | value = value / 100; 109 | let month = value % 100; 110 | value = value / 100; 111 | let year = value; 112 | 113 | Ok(DateTime { 114 | year: year as u16, 115 | month: month as u8, 116 | day: day as u8, 117 | hour: hour as u8, 118 | minute: minute as u8, 119 | second: second as u8, 120 | millis: 0, 121 | }) 122 | } 123 | 124 | pub fn parse_date_time2(cursor: &mut Cursor<&[u8]>, metadata: u16) -> Result { 125 | let value = cursor.read_uint::(5)?; 126 | let millis = parse_fractional_part(cursor, metadata)? / 1000; 127 | 128 | // 1 bit sign(always true). 17 bits year*13+month. 5 bits day. 5 bits hour. 6 bits minute. 6 bits second. 129 | let year_month = (value >> 22) % (1 << 17); 130 | let year = year_month / 13; 131 | let month = year_month % 13; 132 | let day = (value >> 17) % (1 << 5); 133 | let hour = (value >> 12) % (1 << 5); 134 | let minute = (value >> 6) % (1 << 6); 135 | let second = value % (1 << 6); 136 | 137 | Ok(DateTime { 138 | year: year as u16, 139 | month: month as u8, 140 | day: day as u8, 141 | hour: hour as u8, 142 | minute: minute as u8, 143 | second: second as u8, 144 | millis: millis as u32, 145 | }) 146 | } 147 | 148 | pub fn parse_timestamp(cursor: &mut Cursor<&[u8]>, _metadata: u16) -> Result { 149 | let seconds = cursor.read_u32::()? as u64; 150 | Ok(seconds * 1000) 151 | } 152 | 153 | pub fn parse_timestamp2(cursor: &mut Cursor<&[u8]>, metadata: u16) -> Result { 154 | let seconds = cursor.read_u32::()? as u64; 155 | let millisecond = parse_fractional_part(cursor, metadata)? / 1000; 156 | let timestamp = seconds * 1000 + millisecond; 157 | Ok(timestamp) 158 | } 159 | 160 | fn parse_fractional_part(cursor: &mut Cursor<&[u8]>, metadata: u16) -> Result { 161 | let length = (metadata + 1) / 2; 162 | if length == 0 { 163 | return Ok(0); 164 | } 165 | 166 | let fraction = cursor.read_uint::(length as usize)?; 167 | Ok(fraction * u64::pow(100, 3 - length as u32)) 168 | } 169 | -------------------------------------------------------------------------------- /src/events/row_events/decimal.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | use byteorder::{BigEndian, ReadBytesExt}; 3 | use std::io::{Cursor, Read}; 4 | 5 | /// See Docs 6 | 7 | const DIGITS_PER_INT: u8 = 9; 8 | const COMPRESSED_BYTES: [u8; 10] = [0, 1, 1, 2, 2, 3, 3, 4, 4, 4]; 9 | 10 | pub fn parse_decimal(cursor: &mut Cursor<&[u8]>, metadata: u16) -> Result { 11 | let precision = metadata & 0xFF; 12 | let scale = (metadata >> 8) as u8; 13 | let integral = (precision - scale as u16) as u8; 14 | 15 | let uncompressed_integral = integral / DIGITS_PER_INT; 16 | let uncompressed_fractional = scale / DIGITS_PER_INT; 17 | let compressed_integral = integral - (uncompressed_integral * DIGITS_PER_INT); 18 | let compressed_fractional = scale - (uncompressed_fractional * DIGITS_PER_INT); 19 | 20 | let length = (uncompressed_integral << 2) 21 | + COMPRESSED_BYTES[compressed_integral as usize] 22 | + (uncompressed_fractional << 2) 23 | + COMPRESSED_BYTES[compressed_fractional as usize]; 24 | 25 | // Format 26 | // [1-3 bytes] [4 bytes] [4 bytes] [4 bytes] [4 bytes] [1-3 bytes] 27 | // [Compressed] [Uncompressed] [Uncompressed] . [Uncompressed] [Uncompressed] [Compressed] 28 | let mut value = vec![0; length as usize]; 29 | cursor.read_exact(&mut value)?; 30 | let mut result = String::new(); 31 | 32 | let negative = (value[0] & 0x80) == 0; 33 | value[0] ^= 0x80; 34 | 35 | if negative { 36 | result += "-"; 37 | for i in 0..value.len() { 38 | value[i] ^= 0xFF; 39 | } 40 | } 41 | 42 | let mut buffer = Cursor::new(value.as_slice()); 43 | 44 | let mut started = false; 45 | let mut size = COMPRESSED_BYTES[compressed_integral as usize]; 46 | 47 | if size > 0 { 48 | let number = buffer.read_uint::(size as usize)? as u32; 49 | if number > 0 { 50 | started = true; 51 | result += &number.to_string(); 52 | } 53 | } 54 | for _i in 0..uncompressed_integral { 55 | let number = buffer.read_u32::()?; 56 | if started { 57 | result += &format!("{val:0prec$}", prec = 9, val = number) 58 | } else if number > 0 { 59 | started = true; 60 | result += &number.to_string(); 61 | } 62 | } 63 | 64 | // There has to be at least 0 65 | if !started { 66 | result += "0"; 67 | } 68 | if scale > 0 { 69 | result += "."; 70 | } 71 | 72 | size = COMPRESSED_BYTES[compressed_fractional as usize]; 73 | for _i in 0..uncompressed_fractional { 74 | let value = buffer.read_u32::()?; 75 | result += &format!("{val:0prec$}", prec = 9, val = value) 76 | } 77 | if size > 0 { 78 | let value = buffer.read_uint::(size as usize)? as u32; 79 | let precision = compressed_fractional as usize; 80 | result += &format!("{val:0prec$}", prec = precision, val = value) 81 | } 82 | Ok(result) 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use crate::events::row_events::decimal::parse_decimal; 88 | use byteorder::{LittleEndian, ReadBytesExt}; 89 | use std::io::Cursor; 90 | 91 | #[test] 92 | fn parse_positive_number() { 93 | // decimal(65,10), column = '1234567890112233445566778899001112223334445556667778889.9900011112' 94 | let payload: Vec = vec![ 95 | 65, 10, 129, 13, 251, 56, 210, 6, 176, 139, 229, 33, 200, 92, 19, 0, 16, 248, 159, 19, 96 | 239, 59, 244, 39, 205, 127, 73, 59, 2, 55, 215, 2, 97 | ]; 98 | let mut cursor = Cursor::new(payload.as_slice()); 99 | let metadata = cursor.read_u16::().unwrap(); 100 | 101 | let expected = 102 | String::from("1234567890112233445566778899001112223334445556667778889.9900011112"); 103 | assert_eq!(expected, parse_decimal(&mut cursor, metadata).unwrap()); 104 | } 105 | 106 | #[test] 107 | fn parse_negative_number() { 108 | // decimal(65,10), column = '-1234567890112233445566778899001112223334445556667778889.9900011112' 109 | let payload: Vec = vec![ 110 | 65, 10, 126, 242, 4, 199, 45, 249, 79, 116, 26, 222, 55, 163, 236, 255, 239, 7, 96, 111 | 236, 16, 196, 11, 216, 50, 128, 182, 196, 253, 200, 40, 253, 112 | ]; 113 | let mut cursor = Cursor::new(payload.as_slice()); 114 | let metadata = cursor.read_u16::().unwrap(); 115 | 116 | let expected = 117 | String::from("-1234567890112233445566778899001112223334445556667778889.9900011112"); 118 | assert_eq!(expected, parse_decimal(&mut cursor, metadata).unwrap()); 119 | } 120 | 121 | #[test] 122 | fn parse_with_starting_zeros_ignored() { 123 | // decimal(65,10), column = '7778889.9900011112' 124 | let payload: Vec = vec![ 125 | 65, 10, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 118, 178, 126 | 73, 59, 2, 55, 215, 2, 127 | ]; 128 | let mut cursor = Cursor::new(payload.as_slice()); 129 | let metadata = cursor.read_u16::().unwrap(); 130 | 131 | let expected = String::from("7778889.9900011112"); 132 | assert_eq!(expected, parse_decimal(&mut cursor, metadata).unwrap()); 133 | } 134 | 135 | #[test] 136 | fn parse_with_integral_zero() { 137 | // decimal(65,10), column = '.9900011112' 138 | let payload: Vec = vec![ 139 | 65, 10, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 140 | 59, 2, 55, 215, 2, 141 | ]; 142 | let mut cursor = Cursor::new(payload.as_slice()); 143 | let metadata = cursor.read_u16::().unwrap(); 144 | 145 | let expected = String::from("0.9900011112"); 146 | assert_eq!(expected, parse_decimal(&mut cursor, metadata).unwrap()); 147 | } 148 | 149 | #[test] 150 | fn compressed_fractional_starting_zeros_preserved() { 151 | // In this test first two zeros are preserved->[uncompr][comp] 152 | // decimal(60,15), column = '34445556667778889.123456789006700' 153 | let payload: Vec = vec![ 154 | 60, 15, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 13, 152, 244, 39, 205, 127, 73, 7, 91, 155 | 205, 21, 0, 26, 44, 156 | ]; 157 | let mut cursor = Cursor::new(payload.as_slice()); 158 | let metadata = cursor.read_u16::().unwrap(); 159 | 160 | let expected = String::from("34445556667778889.123456789006700"); 161 | assert_eq!(expected, parse_decimal(&mut cursor, metadata).unwrap()); 162 | } 163 | 164 | #[test] 165 | fn parse_integer() { 166 | // decimal(60,0), column = '34445556667778889' 167 | let payload: Vec = vec![ 168 | 60, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 13, 152, 244, 39, 169 | 205, 127, 73, 170 | ]; 171 | let mut cursor = Cursor::new(payload.as_slice()); 172 | let metadata = cursor.read_u16::().unwrap(); 173 | 174 | let expected = String::from("34445556667778889"); 175 | assert_eq!(expected, parse_decimal(&mut cursor, metadata).unwrap()); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/events/row_events/delete_rows_event.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | use crate::events::row_events::row_data::RowData; 3 | use crate::events::row_events::row_parser::{parse_head, parse_row_data_list}; 4 | use crate::events::table_map_event::TableMapEvent; 5 | use crate::extensions::read_bitmap_little_endian; 6 | use std::collections::HashMap; 7 | use std::io::Cursor; 8 | 9 | /// Represents one or many deleted rows in row based replication. 10 | /// See more 11 | #[derive(Debug)] 12 | pub struct DeleteRowsEvent { 13 | /// Gets id of the table where rows were deleted 14 | pub table_id: u64, 15 | 16 | /// Gets flags 17 | pub flags: u16, 18 | 19 | /// Gets number of columns in the table 20 | pub columns_number: usize, 21 | 22 | /// Gets bitmap of columns present in row event. See binlog_row_image parameter. 23 | pub columns_present: Vec, 24 | 25 | /// Gets deleted rows 26 | pub rows: Vec, 27 | } 28 | 29 | impl DeleteRowsEvent { 30 | /// Supports all versions of MariaDB and MySQL 5.5+ (V1 and V2 row events). 31 | pub fn parse( 32 | cursor: &mut Cursor<&[u8]>, 33 | table_map: &HashMap, 34 | row_event_version: u8, 35 | ) -> Result { 36 | let (table_id, flags, columns_number) = parse_head(cursor, row_event_version)?; 37 | let columns_present = read_bitmap_little_endian(cursor, columns_number)?; 38 | let rows = parse_row_data_list(cursor, table_map, table_id, &columns_present)?; 39 | Ok(Self { 40 | table_id, 41 | flags, 42 | columns_number, 43 | columns_present, 44 | rows, 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/events/row_events/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod delete_rows_event; 2 | pub mod mysql_value; 3 | pub mod row_data; 4 | pub mod update_rows_event; 5 | pub mod write_rows_event; 6 | 7 | mod actual_string_type; 8 | mod col_parser; 9 | mod decimal; 10 | mod row_parser; 11 | -------------------------------------------------------------------------------- /src/events/row_events/mysql_value.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct Date { 3 | pub year: u16, 4 | pub month: u8, 5 | pub day: u8, 6 | } 7 | 8 | #[derive(Debug)] 9 | pub struct Time { 10 | pub hour: i16, // Signed value from -838 to 838 11 | pub minute: u8, 12 | pub second: u8, 13 | pub millis: u32, 14 | } 15 | 16 | #[derive(Debug)] 17 | pub struct DateTime { 18 | pub year: u16, 19 | pub month: u8, 20 | pub day: u8, 21 | pub hour: u8, 22 | pub minute: u8, 23 | pub second: u8, 24 | pub millis: u32, 25 | } 26 | 27 | #[derive(Debug)] 28 | pub enum MySqlValue { 29 | TinyInt(u8), 30 | SmallInt(u16), 31 | MediumInt(u32), 32 | Int(u32), 33 | BigInt(u64), 34 | Float(f32), 35 | Double(f64), 36 | Decimal(String), 37 | String(String), 38 | Bit(Vec), 39 | Enum(u32), 40 | Set(u64), 41 | Blob(Vec), 42 | Year(u16), 43 | Date(Date), 44 | Time(Time), 45 | DateTime(DateTime), 46 | Timestamp(u64), // millis from unix time 47 | } 48 | -------------------------------------------------------------------------------- /src/events/row_events/row_data.rs: -------------------------------------------------------------------------------- 1 | use crate::events::row_events::mysql_value::MySqlValue; 2 | 3 | /// Represents an inserted or deleted row in row based replication. 4 | #[derive(Debug)] 5 | pub struct RowData { 6 | /// Column values of the changed row. 7 | pub cells: Vec>, 8 | } 9 | 10 | impl RowData { 11 | pub fn new(cells: Vec>) -> Self { 12 | Self { cells } 13 | } 14 | } 15 | 16 | /// Represents an updated row in row based replication. 17 | #[derive(Debug)] 18 | pub struct UpdateRowData { 19 | /// Row state before it was updated. 20 | pub before_update: RowData, 21 | 22 | /// Actual row state after update. 23 | pub after_update: RowData, 24 | } 25 | 26 | impl UpdateRowData { 27 | pub fn new(before_update: RowData, after_update: RowData) -> Self { 28 | Self { 29 | before_update, 30 | after_update, 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/events/row_events/row_parser.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::column_type::ColumnType; 2 | use crate::errors::Error; 3 | use crate::events::row_events::col_parser::{ 4 | parse_bit, parse_blob, parse_date, parse_date_time, parse_date_time2, parse_string, parse_time, 5 | parse_time2, parse_timestamp, parse_timestamp2, parse_year, 6 | }; 7 | use crate::events::row_events::mysql_value::MySqlValue; 8 | use crate::events::row_events::row_data::{RowData, UpdateRowData}; 9 | use crate::events::table_map_event::TableMapEvent; 10 | use crate::extensions::{read_bitmap_little_endian, read_len_enc_num}; 11 | use byteorder::{LittleEndian, ReadBytesExt}; 12 | use std::collections::HashMap; 13 | use std::io::{Cursor, Seek, SeekFrom}; 14 | 15 | use super::actual_string_type::get_actual_string_type; 16 | use super::decimal::parse_decimal; 17 | 18 | pub const TABLE_MAP_NOT_FOUND: &str = 19 | "No preceding TableMapEvent event was found for the row event. \ 20 | You possibly started replication in the middle of logical event group."; 21 | 22 | /// Parsing row based events. 23 | /// See MariaDB rows version 1 24 | /// See MySQL rows version 1/2 25 | /// See AbstractRowsEventDataDeserializer 26 | 27 | pub fn parse_row_data_list( 28 | cursor: &mut Cursor<&[u8]>, 29 | table_map: &HashMap, 30 | table_id: u64, 31 | columns_present: &Vec, 32 | ) -> Result, Error> { 33 | let table = match table_map.get(&table_id) { 34 | Some(x) => x, 35 | None => return Err(Error::String(TABLE_MAP_NOT_FOUND.to_string())), 36 | }; 37 | 38 | let cells_included = get_bits_number(columns_present); 39 | let mut rows = Vec::new(); 40 | while cursor.position() < cursor.get_ref().len() as u64 { 41 | rows.push(parse_row(cursor, table, columns_present, cells_included)?); 42 | } 43 | Ok(rows) 44 | } 45 | 46 | pub fn parse_update_row_data_list( 47 | cursor: &mut Cursor<&[u8]>, 48 | table_map: &HashMap, 49 | table_id: u64, 50 | columns_before_update: &Vec, 51 | columns_after_update: &Vec, 52 | ) -> Result, Error> { 53 | let table = match table_map.get(&table_id) { 54 | Some(x) => x, 55 | None => return Err(Error::String(TABLE_MAP_NOT_FOUND.to_string())), 56 | }; 57 | 58 | let cells_included_before_update = get_bits_number(columns_before_update); 59 | let cells_included_after_update = get_bits_number(columns_after_update); 60 | let mut rows = Vec::new(); 61 | while cursor.position() < cursor.get_ref().len() as u64 { 62 | let row_before_update = parse_row( 63 | cursor, 64 | table, 65 | columns_before_update, 66 | cells_included_before_update, 67 | )?; 68 | let row_after_update = parse_row( 69 | cursor, 70 | table, 71 | columns_after_update, 72 | cells_included_after_update, 73 | )?; 74 | rows.push(UpdateRowData::new(row_before_update, row_after_update)); 75 | } 76 | Ok(rows) 77 | } 78 | 79 | pub fn parse_head( 80 | cursor: &mut Cursor<&[u8]>, 81 | row_event_version: u8, 82 | ) -> Result<(u64, u16, usize), Error> { 83 | let table_id = cursor.read_u48::()?; 84 | let flags = cursor.read_u16::()?; 85 | 86 | // Ignore extra data from newer versions of events 87 | if row_event_version == 2 { 88 | let extra_data_length = cursor.read_u16::()?; 89 | let skip = extra_data_length as i64 - 2; 90 | cursor.seek(SeekFrom::Current(skip))?; 91 | } 92 | 93 | let columns_number = read_len_enc_num(cursor)?; 94 | Ok((table_id, flags, columns_number)) 95 | } 96 | 97 | pub fn parse_row( 98 | cursor: &mut Cursor<&[u8]>, 99 | table_map: &TableMapEvent, 100 | columns_present: &Vec, 101 | cells_included: usize, 102 | ) -> Result { 103 | let mut row = Vec::with_capacity(table_map.column_types.len()); 104 | let null_bitmap = read_bitmap_little_endian(cursor, cells_included)?; 105 | 106 | let mut skipped_columns = 0; 107 | for i in 0..table_map.column_types.len() { 108 | // Data is missing if binlog_row_image != full 109 | if !columns_present[i] { 110 | skipped_columns += 1; 111 | row.push(None); 112 | } 113 | // Column is present and has null value 114 | else if null_bitmap[i - skipped_columns] { 115 | row.push(None); 116 | } 117 | // Column has data 118 | else { 119 | let mut column_type = table_map.column_types[i]; 120 | let mut metadata = table_map.column_metadata[i]; 121 | if ColumnType::from_code(column_type)? == ColumnType::String { 122 | get_actual_string_type(&mut column_type, &mut metadata); 123 | } 124 | row.push(Some(parse_cell(cursor, column_type, metadata)?)); 125 | } 126 | } 127 | Ok(RowData::new(row)) 128 | } 129 | 130 | fn parse_cell( 131 | cursor: &mut Cursor<&[u8]>, 132 | column_type: u8, 133 | metadata: u16, 134 | ) -> Result { 135 | let value = match ColumnType::from_code(column_type)? { 136 | /* Numeric types. The only place where numbers can be negative */ 137 | ColumnType::Tiny => MySqlValue::TinyInt(cursor.read_u8()?), 138 | ColumnType::Short => MySqlValue::SmallInt(cursor.read_u16::()?), 139 | ColumnType::Int24 => MySqlValue::MediumInt(cursor.read_u24::()?), 140 | ColumnType::Long => MySqlValue::Int(cursor.read_u32::()?), 141 | ColumnType::LongLong => MySqlValue::BigInt(cursor.read_u64::()?), 142 | ColumnType::Float => MySqlValue::Float(cursor.read_f32::()?), 143 | ColumnType::Double => MySqlValue::Double(cursor.read_f64::()?), 144 | ColumnType::NewDecimal => MySqlValue::Decimal(parse_decimal(cursor, metadata)?), 145 | /* String types, includes varchar, varbinary & fixed char, binary */ 146 | ColumnType::String => MySqlValue::String(parse_string(cursor, metadata)?), 147 | ColumnType::VarChar => MySqlValue::String(parse_string(cursor, metadata)?), 148 | ColumnType::VarString => MySqlValue::String(parse_string(cursor, metadata)?), 149 | /* BIT, ENUM, SET types */ 150 | ColumnType::Bit => MySqlValue::Bit(parse_bit(cursor, metadata)?), 151 | ColumnType::Enum => { 152 | MySqlValue::Enum(cursor.read_uint::(metadata as usize)? as u32) 153 | } 154 | ColumnType::Set => { 155 | MySqlValue::Set(cursor.read_uint::(metadata as usize)? as u64) 156 | } 157 | /* Blob types. MariaDB always creates BLOB for first three */ 158 | ColumnType::TinyBlob => MySqlValue::Blob(parse_blob(cursor, metadata)?), 159 | ColumnType::MediumBlob => MySqlValue::Blob(parse_blob(cursor, metadata)?), 160 | ColumnType::LongBlob => MySqlValue::Blob(parse_blob(cursor, metadata)?), 161 | ColumnType::Blob => MySqlValue::Blob(parse_blob(cursor, metadata)?), 162 | /* Date and time types */ 163 | ColumnType::Year => MySqlValue::Year(parse_year(cursor, metadata)?), 164 | ColumnType::Date => MySqlValue::Date(parse_date(cursor, metadata)?), 165 | // Older versions of MySQL. 166 | ColumnType::Time => MySqlValue::Time(parse_time(cursor, metadata)?), 167 | ColumnType::TimeStamp => MySqlValue::Timestamp(parse_timestamp(cursor, metadata)?), 168 | ColumnType::DateTime => MySqlValue::DateTime(parse_date_time(cursor, metadata)?), 169 | // MySQL 5.6.4+ types. Supported from MariaDB 10.1.2. 170 | ColumnType::Time2 => MySqlValue::Time(parse_time2(cursor, metadata)?), 171 | ColumnType::TimeStamp2 => MySqlValue::Timestamp(parse_timestamp2(cursor, metadata)?), 172 | ColumnType::DateTime2 => MySqlValue::DateTime(parse_date_time2(cursor, metadata)?), 173 | /* MySQL-specific data types */ 174 | ColumnType::Geometry => MySqlValue::Blob(parse_blob(cursor, metadata)?), 175 | ColumnType::Json => MySqlValue::Blob(parse_blob(cursor, metadata)?), 176 | _ => { 177 | return Err(Error::String(format!( 178 | "Parsing column type {:?} is not supported", 179 | ColumnType::from_code(column_type)? 180 | ))) 181 | } 182 | }; 183 | Ok(value) 184 | } 185 | 186 | /// Gets number of bits set in a bitmap. 187 | fn get_bits_number(bitmap: &Vec) -> usize { 188 | bitmap.iter().filter(|&x| *x == true).count() 189 | } 190 | -------------------------------------------------------------------------------- /src/events/row_events/update_rows_event.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | use crate::events::row_events::row_data::UpdateRowData; 3 | use crate::events::row_events::row_parser::{parse_head, parse_update_row_data_list}; 4 | use crate::events::table_map_event::TableMapEvent; 5 | use crate::extensions::read_bitmap_little_endian; 6 | use std::collections::HashMap; 7 | use std::io::Cursor; 8 | 9 | /// Represents one or many updated rows in row based replication. 10 | /// Includes versions before and after update. 11 | /// See more 12 | #[derive(Debug)] 13 | pub struct UpdateRowsEvent { 14 | /// Gets id of the table where rows were updated 15 | pub table_id: u64, 16 | 17 | /// Gets flags 18 | pub flags: u16, 19 | 20 | /// Gets number of columns in the table 21 | pub columns_number: usize, 22 | 23 | /// Gets bitmap of columns present in row event before update. See binlog_row_image parameter. 24 | pub columns_before_update: Vec, 25 | 26 | /// Gets bitmap of columns present in row event after update. See binlog_row_image parameter. 27 | pub columns_after_update: Vec, 28 | 29 | /// Gets updated rows 30 | pub rows: Vec, 31 | } 32 | 33 | impl UpdateRowsEvent { 34 | /// Supports all versions of MariaDB and MySQL 5.5+ (V1 and V2 row events). 35 | pub fn parse( 36 | cursor: &mut Cursor<&[u8]>, 37 | table_map: &HashMap, 38 | row_event_version: u8, 39 | ) -> Result { 40 | let (table_id, flags, columns_number) = parse_head(cursor, row_event_version)?; 41 | let columns_before_update = read_bitmap_little_endian(cursor, columns_number)?; 42 | let columns_after_update = read_bitmap_little_endian(cursor, columns_number)?; 43 | let rows = parse_update_row_data_list( 44 | cursor, 45 | table_map, 46 | table_id, 47 | &columns_before_update, 48 | &columns_after_update, 49 | )?; 50 | Ok(Self { 51 | table_id, 52 | flags, 53 | columns_number, 54 | columns_before_update, 55 | columns_after_update, 56 | rows, 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/events/row_events/write_rows_event.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | use crate::events::row_events::row_data::RowData; 3 | use crate::events::row_events::row_parser::{parse_head, parse_row_data_list}; 4 | use crate::events::table_map_event::TableMapEvent; 5 | use crate::extensions::read_bitmap_little_endian; 6 | use std::collections::HashMap; 7 | use std::io::Cursor; 8 | 9 | /// Represents one or many inserted rows in row based replication. 10 | /// See more 11 | #[derive(Debug)] 12 | pub struct WriteRowsEvent { 13 | /// Gets id of the table where rows were inserted 14 | pub table_id: u64, 15 | 16 | /// Gets flags 17 | pub flags: u16, 18 | 19 | /// Gets number of columns in the table 20 | pub columns_number: usize, 21 | 22 | /// Gets bitmap of columns present in row event. See binlog_row_image parameter. 23 | pub columns_present: Vec, 24 | 25 | /// Gets inserted rows 26 | pub rows: Vec, 27 | } 28 | 29 | impl WriteRowsEvent { 30 | /// Supports all versions of MariaDB and MySQL 5.5+ (V1 and V2 row events). 31 | pub fn parse( 32 | cursor: &mut Cursor<&[u8]>, 33 | table_map: &HashMap, 34 | row_event_version: u8, 35 | ) -> Result { 36 | let (table_id, flags, columns_number) = parse_head(cursor, row_event_version)?; 37 | let columns_present = read_bitmap_little_endian(cursor, columns_number)?; 38 | let rows = parse_row_data_list(cursor, table_map, table_id, &columns_present)?; 39 | Ok(Self { 40 | table_id, 41 | flags, 42 | columns_number, 43 | columns_present, 44 | rows, 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/events/rows_query_event.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Cursor, Read, Seek, SeekFrom}; 2 | 3 | use crate::errors::Error; 4 | 5 | /// Represents query that caused row events. 6 | /// See MySQL docs 7 | /// See MariaDB docs 8 | #[derive(Debug)] 9 | pub struct RowsQueryEvent { 10 | /// Gets SQL statement 11 | pub query: String, 12 | } 13 | 14 | impl RowsQueryEvent { 15 | /// Supports MySQL 5.6+. 16 | pub fn parse_mysql(cursor: &mut Cursor<&[u8]>) -> Result { 17 | cursor.seek(SeekFrom::Current(1))?; 18 | 19 | let mut query = String::new(); 20 | cursor.read_to_string(&mut query)?; 21 | 22 | Ok(Self { query }) 23 | } 24 | 25 | /// Supports MariaDB 5.3+. 26 | pub fn parse_mariadb(cursor: &mut Cursor<&[u8]>) -> Result { 27 | let mut query = String::new(); 28 | cursor.read_to_string(&mut query)?; 29 | 30 | Ok(Self { query }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/events/table_map_event.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::column_type::ColumnType; 2 | use crate::errors::Error; 3 | use crate::extensions::{read_bitmap_little_endian, read_len_enc_num, read_string}; 4 | use crate::metadata::table_metadata::TableMetadata; 5 | use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; 6 | use std::io::{Cursor, Read, Seek, SeekFrom}; 7 | 8 | /// The event has table defition for row events. 9 | /// See more 10 | #[derive(Clone, Debug)] 11 | pub struct TableMapEvent { 12 | /// Gets id of the changed table 13 | pub table_id: u64, 14 | 15 | /// Gets database name of the changed table 16 | pub database_name: String, 17 | 18 | /// Gets name of the changed table 19 | pub table_name: String, 20 | 21 | /// Gets column types of the changed table 22 | pub column_types: Vec, 23 | 24 | /// Gets columns metadata 25 | pub column_metadata: Vec, 26 | 27 | /// Gets columns nullability 28 | pub null_bitmap: Vec, 29 | 30 | /// Gets table metadata for MySQL 5.6+ 31 | pub table_metadata: Option, 32 | } 33 | 34 | impl TableMapEvent { 35 | /// Supports all versions of MariaDB and MySQL 5.0+. 36 | pub fn parse(cursor: &mut Cursor<&[u8]>) -> Result { 37 | let table_id = cursor.read_u48::()?; 38 | 39 | // Reserved bytes 40 | cursor.seek(SeekFrom::Current(2))?; 41 | 42 | // Database name is null terminated 43 | let database_name_length = cursor.read_u8()?; 44 | let database_name = read_string(cursor, database_name_length as usize)?; 45 | cursor.seek(SeekFrom::Current(1))?; 46 | 47 | // Table name is null terminated 48 | let table_name_length = cursor.read_u8()?; 49 | let table_name = read_string(cursor, table_name_length as usize)?; 50 | cursor.seek(SeekFrom::Current(1))?; 51 | 52 | let columns_number = read_len_enc_num(cursor)?; 53 | let mut column_types = vec![0u8; columns_number]; 54 | cursor.read_exact(&mut column_types)?; 55 | 56 | let _metadata_length = read_len_enc_num(cursor)?; 57 | let column_metadata = TableMapEvent::parse_metadata(cursor, &column_types)?; 58 | 59 | let null_bitmap = read_bitmap_little_endian(cursor, columns_number)?; 60 | 61 | let mut table_metadata = None; 62 | if cursor.position() < cursor.get_ref().len() as u64 { 63 | // Table metadata is supported in MySQL 5.6+ and MariaDB 10.5+. 64 | table_metadata = Some(TableMetadata::parse(cursor, &column_types)?); 65 | } 66 | 67 | Ok(Self { 68 | table_id, 69 | database_name, 70 | table_name, 71 | column_types, 72 | column_metadata, 73 | null_bitmap, 74 | table_metadata, 75 | }) 76 | } 77 | 78 | fn parse_metadata( 79 | cursor: &mut Cursor<&[u8]>, 80 | column_types: &Vec, 81 | ) -> Result, Error> { 82 | let mut metadata = vec![0u16; column_types.len()]; 83 | 84 | // See https://mariadb.com/kb/en/library/rows_event_v1/#column-data-formats 85 | for i in 0..column_types.len() { 86 | let column_type = ColumnType::from_code(column_types[i])?; 87 | metadata[i] = match column_type { 88 | // 1 byte metadata 89 | ColumnType::Geometry => cursor.read_u8()? as u16, 90 | ColumnType::Json => cursor.read_u8()? as u16, 91 | ColumnType::TinyBlob => cursor.read_u8()? as u16, 92 | ColumnType::MediumBlob => cursor.read_u8()? as u16, 93 | ColumnType::LongBlob => cursor.read_u8()? as u16, 94 | ColumnType::Blob => cursor.read_u8()? as u16, 95 | ColumnType::Float => cursor.read_u8()? as u16, 96 | ColumnType::Double => cursor.read_u8()? as u16, 97 | ColumnType::TimeStamp2 => cursor.read_u8()? as u16, 98 | ColumnType::DateTime2 => cursor.read_u8()? as u16, 99 | ColumnType::Time2 => cursor.read_u8()? as u16, 100 | // 2 bytes little endian 101 | ColumnType::Bit => cursor.read_u16::()?, 102 | ColumnType::VarChar => cursor.read_u16::()?, 103 | ColumnType::VarString => cursor.read_u16::()?, 104 | ColumnType::NewDecimal => cursor.read_u16::()?, 105 | // 2 bytes big endian 106 | ColumnType::Enum => cursor.read_u16::()?, 107 | ColumnType::Set => cursor.read_u16::()?, 108 | ColumnType::String => cursor.read_u16::()?, 109 | _ => 0, 110 | } 111 | } 112 | Ok(metadata) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/events/uservar_event.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, ReadBytesExt}; 2 | use std::io::Cursor; 3 | 4 | use crate::{errors::Error, extensions::read_string}; 5 | 6 | /// A USER_VAR_EVENT is written every time a statement uses a user defined variable. 7 | /// See more 8 | #[derive(Debug)] 9 | pub struct UserVarEvent { 10 | /// User variable name 11 | pub name: String, 12 | 13 | /// User variable value 14 | pub value: Option, 15 | } 16 | 17 | /// User variable value 18 | #[derive(Debug)] 19 | pub struct VariableValue { 20 | /// Variable type 21 | pub var_type: u8, 22 | 23 | /// Collation number 24 | pub collation: u32, 25 | 26 | /// User variable value 27 | pub value: String, 28 | 29 | /// flags 30 | pub flags: u8, 31 | } 32 | 33 | impl UserVarEvent { 34 | /// Supports all versions of MariaDB and MySQL. 35 | pub fn parse(cursor: &mut Cursor<&[u8]>) -> Result { 36 | let name_len = cursor.read_u32::()?; 37 | let name = read_string(cursor, name_len as usize)?; 38 | 39 | let is_null = cursor.read_u8()? != 0; // 0 indicates there is a value; 40 | if is_null { 41 | return Ok(Self { name, value: None }); 42 | } 43 | 44 | let var_type = cursor.read_u8()?; 45 | let collation = cursor.read_u32::()?; 46 | 47 | let value_len = cursor.read_u32::()?; 48 | let value = read_string(cursor, value_len as usize)?; 49 | 50 | let flags = cursor.read_u8()?; 51 | 52 | Ok(Self { 53 | name, 54 | value: Some(VariableValue { 55 | var_type, 56 | collation, 57 | value, 58 | flags, 59 | }), 60 | }) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use std::io::Cursor; 67 | 68 | use crate::events::uservar_event::UserVarEvent; 69 | 70 | #[test] 71 | fn parse_user_var_event() { 72 | let payload: Vec = vec![ 73 | 0x03, 0x00, 0x00, 0x00, 0x66, 0x6f, 0x6f, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x03, 74 | 0x00, 0x00, 0x00, 0x62, 0x61, 0x72, 0x6b, 0x3d, 0xd9, 0x7d, 75 | ]; 76 | let mut cursor = Cursor::new(payload.as_slice()); 77 | 78 | let event = UserVarEvent::parse(&mut cursor).unwrap(); 79 | assert_eq!(String::from("foo"), event.name); 80 | assert_eq!(false, event.value.is_none()); 81 | 82 | let variable = event.value.unwrap(); 83 | assert_eq!(0, variable.var_type); 84 | assert_eq!(33, variable.collation); 85 | assert_eq!(String::from("bar"), variable.value); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/events/xid_event.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, ReadBytesExt}; 2 | use std::io::Cursor; 3 | 4 | use crate::errors::Error; 5 | 6 | /// Represents a transaction commit event. 7 | /// See more 8 | #[derive(Debug)] 9 | pub struct XidEvent { 10 | /// Gets the XID transaction number 11 | pub xid: u64, 12 | } 13 | 14 | impl XidEvent { 15 | /// Supports all versions of MariaDB and MySQL. 16 | pub fn parse(cursor: &mut Cursor<&[u8]>) -> Result { 17 | let xid = cursor.read_u64::()?; 18 | 19 | Ok(Self { xid }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/extensions.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::auth_plugin_names::AuthPlugin; 2 | use crate::constants::NULL_TERMINATOR; 3 | use crate::errors::Error; 4 | use crate::responses::error_packet::ErrorPacket; 5 | use crate::responses::response_type::ResponseType; 6 | use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; 7 | use sha1::Sha1; 8 | use sha2::{Digest, Sha256}; 9 | use std::io::{self, BufRead, Cursor, Read, Write}; 10 | 11 | pub fn encrypt_password(password: &String, scramble: &String, auth_plugin: &AuthPlugin) -> Vec { 12 | match auth_plugin { 13 | AuthPlugin::MySqlNativePassword => { 14 | let password_hash = sha1(password.as_bytes()); 15 | let concat_hash = [scramble.as_bytes().to_vec(), sha1(&password_hash)].concat(); 16 | xor(&password_hash, &sha1(&concat_hash)) 17 | } 18 | AuthPlugin::CachingSha2Password => { 19 | let password_hash = sha256(password.as_bytes()); 20 | let concat_hash = [scramble.as_bytes().to_vec(), sha256(&password_hash)].concat(); 21 | xor(&password_hash, &sha256(&concat_hash)) 22 | } 23 | } 24 | } 25 | 26 | pub fn sha1(value: &[u8]) -> Vec { 27 | let mut hasher = Sha1::new(); 28 | hasher.update(value); 29 | hasher.finalize().as_slice().to_vec() 30 | } 31 | 32 | pub fn sha256(value: &[u8]) -> Vec { 33 | let mut hasher = Sha256::new(); 34 | hasher.update(value); 35 | hasher.finalize().as_slice().to_vec() 36 | } 37 | 38 | pub fn xor(slice1: &[u8], slice2: &[u8]) -> Vec { 39 | let mut result = vec![0u8; slice1.len()]; 40 | for i in 0..result.len() { 41 | result[i] = slice1[i] ^ slice2[i % slice2.len()]; 42 | } 43 | result 44 | } 45 | 46 | pub fn read_null_term_string(cursor: &mut Cursor<&[u8]>) -> Result { 47 | let mut vec = Vec::new(); 48 | cursor.read_until(NULL_TERMINATOR, &mut vec)?; 49 | vec.pop(); 50 | Ok(String::from_utf8(vec)?) 51 | } 52 | 53 | pub fn write_null_term_string( 54 | cursor: &mut Cursor<&mut Vec>, 55 | str: &String, 56 | ) -> Result<(), io::Error> { 57 | cursor.write(str.as_bytes())?; 58 | cursor.write_u8(NULL_TERMINATOR)?; 59 | Ok(()) 60 | } 61 | 62 | pub fn read_string(cursor: &mut Cursor<&[u8]>, size: usize) -> Result { 63 | let mut vec = vec![0; size]; 64 | cursor.read_exact(&mut vec)?; 65 | Ok(String::from_utf8(vec)?) 66 | } 67 | 68 | pub fn read_len_enc_str(cursor: &mut Cursor<&[u8]>) -> Result { 69 | let length = read_len_enc_num(cursor)?; 70 | Ok(read_string(cursor, length)?) 71 | } 72 | 73 | /// if first byte is less than 0xFB - Integer value is this 1 byte integer 74 | /// 0xFB - NULL value 75 | /// 0xFC - Integer value is encoded in the next 2 bytes (3 bytes total) 76 | /// 0xFD - Integer value is encoded in the next 3 bytes (4 bytes total) 77 | /// 0xFE - Integer value is encoded in the next 8 bytes (9 bytes total) 78 | pub fn read_len_enc_num(cursor: &mut Cursor<&[u8]>) -> Result { 79 | let first_byte = cursor.read_u8()?; 80 | 81 | if first_byte < 0xFB { 82 | Ok(first_byte as usize) 83 | } else if first_byte == 0xFB { 84 | Err(Error::String( 85 | "Length encoded integer cannot be NULL.".to_string(), 86 | )) 87 | } else if first_byte == 0xFC { 88 | Ok(cursor.read_u16::()? as usize) 89 | } else if first_byte == 0xFD { 90 | Ok(cursor.read_u24::()? as usize) 91 | } else if first_byte == 0xFE { 92 | Ok(cursor.read_u64::()? as usize) 93 | } else { 94 | let value = format!("Unexpected length-encoded integer: {}", first_byte).to_string(); 95 | Err(Error::String(value)) 96 | } 97 | } 98 | 99 | /// Reads bitmap in little-endian bytes order 100 | pub fn read_bitmap_little_endian( 101 | cursor: &mut Cursor<&[u8]>, 102 | bits_number: usize, 103 | ) -> Result, io::Error> { 104 | let mut result = vec![false; bits_number]; 105 | let bytes_number = (bits_number + 7) / 8; 106 | for i in 0..bytes_number { 107 | let value = cursor.read_u8()?; 108 | for y in 0..8 { 109 | let index = (i << 3) + y; 110 | if index == bits_number { 111 | break; 112 | } 113 | result[index] = (value & (1 << y)) > 0; 114 | } 115 | } 116 | Ok(result) 117 | } 118 | 119 | /// Reads bitmap in big-endian bytes order 120 | pub fn read_bitmap_big_endian( 121 | cursor: &mut Cursor<&[u8]>, 122 | bits_number: usize, 123 | ) -> Result, io::Error> { 124 | let mut result = vec![false; bits_number]; 125 | let bytes_number = (bits_number + 7) / 8; 126 | for i in 0..bytes_number { 127 | let value = cursor.read_u8()?; 128 | for y in 0..8 { 129 | let index = ((bytes_number - i - 1) << 3) + y; 130 | if index >= bits_number { 131 | continue; 132 | } 133 | result[index] = (value & (1 << y)) > 0; 134 | } 135 | } 136 | Ok(result) 137 | } 138 | 139 | pub fn check_error_packet(packet: &[u8], message: &str) -> Result<(), Error> { 140 | if packet[0] == ResponseType::ERROR { 141 | let error = ErrorPacket::parse(&packet[1..])?; 142 | let message = format!("{} {:?}", message, error).to_string(); 143 | return Err(Error::String(message)); 144 | } 145 | return Ok(()); 146 | } 147 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # mysql_cdc 2 | //! MySQL/MariaDB binlog replication client for Rust 3 | //! 4 | //! ## Limitations 5 | //! Please note the lib currently has the following limitations: 6 | //! - Supports only standard auth plugins `mysql_native_password` and `caching_sha2_password`. 7 | //! - **Currently, the library doesn't support SSL encryption.** 8 | //! - **Doesn't handle split packets (16MB and more).** 9 | //! 10 | //! ## Binlog event stream replication 11 | //! Real-time replication client works the following way. 12 | //! ```no_run 13 | //! use mysql_cdc::binlog_client::BinlogClient; 14 | //! use mysql_cdc::binlog_options::BinlogOptions; 15 | //! use mysql_cdc::errors::Error; 16 | //! use mysql_cdc::providers::mariadb::gtid::gtid_list::GtidList; 17 | //! use mysql_cdc::providers::mysql::gtid::gtid_set::GtidSet; 18 | //! use mysql_cdc::replica_options::ReplicaOptions; 19 | //! use mysql_cdc::ssl_mode::SslMode; 20 | //! 21 | //! fn main() -> Result<(), Error> { 22 | //! // Start replication from MariaDB GTID 23 | //! let _options = BinlogOptions::from_mariadb_gtid(GtidList::parse("0-1-270")?); 24 | //! 25 | //! // Start replication from MySQL GTID 26 | //! let gtid_set = 27 | //! "d4c17f0c-4f11-11ea-93e3-325d3e1cd1c8:1-107, f442510a-2881-11ea-b1dd-27916133dbb2:1-7"; 28 | //! let _options = BinlogOptions::from_mysql_gtid(GtidSet::parse(gtid_set)?); 29 | //! 30 | //! // Start replication from the position 31 | //! let _options = BinlogOptions::from_position(String::from("mysql-bin.000008"), 195); 32 | //! 33 | //! // Start replication from last master position. 34 | //! // Useful when you are only interested in new changes. 35 | //! let _options = BinlogOptions::from_end(); 36 | //! 37 | //! // Start replication from first event of first available master binlog. 38 | //! // Note that binlog files by default have expiration time and deleted. 39 | //! let options = BinlogOptions::from_start(); 40 | //! 41 | //! let options = ReplicaOptions { 42 | //! username: String::from("root"), 43 | //! password: String::from("Qwertyu1"), 44 | //! blocking: true, 45 | //! ssl_mode: SslMode::Disabled, 46 | //! binlog: options, 47 | //! ..Default::default() 48 | //! }; 49 | //! 50 | //! let mut client = BinlogClient::new(options); 51 | //! 52 | //! for result in client.replicate()? { 53 | //! let (header, event) = result?; 54 | //! println!("{:#?}", header); 55 | //! println!("{:#?}", event); 56 | //! 57 | //! // You process an event here 58 | //! 59 | //! // After you processed the event, you need to update replication position 60 | //! client.commit(&header, &event); 61 | //! } 62 | //! Ok(()) 63 | //! } 64 | //! ``` 65 | //! A typical transaction has the following structure. 66 | //! 1. `GtidEvent` if gtid mode is enabled. 67 | //! 2. One or many `TableMapEvent` events. 68 | //! - One or many `WriteRowsEvent` events. 69 | //! - One or many `UpdateRowsEvent` events. 70 | //! - One or many `DeleteRowsEvent` events. 71 | //! 3. `XidEvent` indicating commit of the transaction. 72 | //! 73 | //! **It's best practice to use GTID replication with the `from_gtid` method.** Using the approach you can correctly perform replication failover. 74 | //! Note that in GTID mode `from_gtid` has the following behavior: 75 | //! - `from_gtid(@@gtid_purged)` acts like `from_start()` 76 | //! - `from_gtid(@@gtid_executed)` acts like `from_end()` 77 | //! 78 | //! ## Reading binlog files offline 79 | //! In some cases you will need to read binlog files offline from the file system. 80 | //! This can be done using `BinlogReader` class. 81 | //! ```no_run 82 | //! use mysql_cdc::{binlog_reader::BinlogReader, errors::Error}; 83 | //! use std::fs::File; 84 | //! 85 | //! const PATH: &str = "mysql-bin.000001"; 86 | //! 87 | //! fn main() -> Result<(), Error> { 88 | //! let file = File::open(PATH)?; 89 | //! let reader = BinlogReader::new(file)?; 90 | //! 91 | //! for result in reader.read_events() { 92 | //! let (header, event) = result?; 93 | //! println!("{:#?}", header); 94 | //! println!("{:#?}", event); 95 | //! } 96 | //! Ok(()) 97 | //! } 98 | //! ``` 99 | 100 | pub mod binlog_client; 101 | pub mod binlog_events; 102 | pub mod binlog_options; 103 | pub mod binlog_reader; 104 | pub mod errors; 105 | pub mod events; 106 | pub mod metadata; 107 | pub mod providers; 108 | pub mod replica_options; 109 | pub mod ssl_mode; 110 | pub mod starting_strategy; 111 | 112 | mod commands; 113 | mod configure; 114 | mod connect; 115 | mod constants; 116 | mod extensions; 117 | mod packet_channel; 118 | mod responses; 119 | -------------------------------------------------------------------------------- /src/metadata/default_charset.rs: -------------------------------------------------------------------------------- 1 | /// Represents charsets of character columns. 2 | #[derive(Clone, Debug)] 3 | pub struct DefaultCharset { 4 | /// Gets the most used charset collation. 5 | pub default_charset_collation: u32, 6 | 7 | /// Gets ColumnIndex-Charset map for columns that don't use the default charset. 8 | pub charset_collations: Vec<(u32, u32)>, 9 | } 10 | 11 | impl DefaultCharset { 12 | pub fn new(default_charset_collation: u32, charset_collations: Vec<(u32, u32)>) -> Self { 13 | Self { 14 | default_charset_collation, 15 | charset_collations, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/metadata/metadata_type.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | 3 | pub enum MetadataType { 4 | Signedness = 1, 5 | DefaultCharset = 2, 6 | ColumnCharset = 3, 7 | ColumnName = 4, 8 | SetStrValue = 5, 9 | EnumStrValue = 6, 10 | GeometryType = 7, 11 | SimplePrimaryKey = 8, 12 | PrimaryKeyWithPrefix = 9, 13 | EnumAndSetDefaultCharset = 10, 14 | EnumAndSetColumnCharset = 11, 15 | ColumnVisibility = 12, 16 | } 17 | 18 | impl MetadataType { 19 | pub fn from_code(code: u8) -> Result { 20 | let value = match code { 21 | 1 => MetadataType::Signedness, 22 | 2 => MetadataType::DefaultCharset, 23 | 3 => MetadataType::ColumnCharset, 24 | 4 => MetadataType::ColumnName, 25 | 5 => MetadataType::SetStrValue, 26 | 6 => MetadataType::EnumStrValue, 27 | 7 => MetadataType::GeometryType, 28 | 8 => MetadataType::SimplePrimaryKey, 29 | 9 => MetadataType::PrimaryKeyWithPrefix, 30 | 10 => MetadataType::EnumAndSetDefaultCharset, 31 | 11 => MetadataType::EnumAndSetColumnCharset, 32 | 12 => MetadataType::ColumnVisibility, 33 | _ => { 34 | return Err(Error::String( 35 | format!("Table metadata type {} is not supported", code).to_string(), 36 | )) 37 | } 38 | }; 39 | Ok(value) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/metadata/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod default_charset; 2 | pub mod metadata_type; 3 | pub mod table_metadata; 4 | -------------------------------------------------------------------------------- /src/metadata/table_metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::column_type::ColumnType; 2 | use crate::errors::Error; 3 | use crate::extensions::{read_len_enc_num, read_len_enc_str}; 4 | use crate::metadata::default_charset::DefaultCharset; 5 | use crate::metadata::metadata_type::MetadataType; 6 | use byteorder::ReadBytesExt; 7 | use std::io::{self, Cursor, Read}; 8 | 9 | /// Contains metadata for table columns. 10 | /// See more 11 | #[derive(Clone, Debug)] 12 | pub struct TableMetadata { 13 | /// Gets signedness of numeric colums. 14 | pub signedness: Option>, 15 | 16 | /// Gets charsets of character columns. 17 | pub default_charset: Option, 18 | 19 | /// Gets charsets of character columns. 20 | pub column_charsets: Option>, 21 | 22 | /// Gets column names. 23 | pub column_names: Option>, 24 | 25 | /// Gets string values of SET columns. 26 | pub set_string_values: Option>>, 27 | 28 | /// Gets string values of ENUM columns 29 | pub enum_string_values: Option>>, 30 | 31 | /// Gets real types of geometry columns. 32 | pub geometry_types: Option>, 33 | 34 | /// Gets primary keys without prefixes. 35 | pub simple_primary_keys: Option>, 36 | 37 | /// Gets primary keys with prefixes. 38 | pub primary_keys_with_prefix: Option>, 39 | 40 | /// Gets charsets of ENUM and SET columns. 41 | pub enum_and_set_default_charset: Option, 42 | 43 | /// Gets charsets of ENUM and SET columns. 44 | pub enum_and_set_column_charsets: Option>, 45 | 46 | /// Gets visibility attribute of columns. 47 | pub column_visibility: Option>, 48 | } 49 | 50 | impl TableMetadata { 51 | pub fn parse(cursor: &mut Cursor<&[u8]>, column_types: &[u8]) -> Result { 52 | let mut signedness = None; 53 | let mut default_charset = None; 54 | let mut column_charsets = None; 55 | let mut column_names = None; 56 | let mut set_string_values = None; 57 | let mut enum_string_values = None; 58 | let mut geometry_types = None; 59 | let mut simple_primary_keys = None; 60 | let mut primary_keys_with_prefix = None; 61 | let mut enum_and_set_default_charset = None; 62 | let mut enum_and_set_column_charsets = None; 63 | let mut column_visibility = None; 64 | 65 | while cursor.position() < cursor.get_ref().len() as u64 { 66 | let metadata_type = MetadataType::from_code(cursor.read_u8()?)?; 67 | let metadata_length = read_len_enc_num(cursor)?; 68 | 69 | let mut metadata = vec![0u8; metadata_length]; 70 | cursor.read_exact(&mut metadata)?; 71 | 72 | let mut buffer = Cursor::new(metadata.as_slice()); 73 | match metadata_type { 74 | MetadataType::Signedness => { 75 | let count = get_numeric_column_count(column_types)?; 76 | signedness = Some(read_bitmap_reverted(&mut buffer, count)?); 77 | } 78 | MetadataType::DefaultCharset => { 79 | default_charset = Some(parse_default_charser(&mut buffer)?); 80 | } 81 | MetadataType::ColumnCharset => { 82 | column_charsets = Some(parse_int_array(&mut buffer)?); 83 | } 84 | MetadataType::ColumnName => { 85 | column_names = Some(parse_string_array(&mut buffer)?); 86 | } 87 | MetadataType::SetStrValue => { 88 | set_string_values = Some(parse_type_values(&mut buffer)?); 89 | } 90 | MetadataType::EnumStrValue => { 91 | enum_string_values = Some(parse_type_values(&mut buffer)?); 92 | } 93 | MetadataType::GeometryType => { 94 | geometry_types = Some(parse_int_array(&mut buffer)?); 95 | } 96 | MetadataType::SimplePrimaryKey => { 97 | simple_primary_keys = Some(parse_int_array(&mut buffer)?); 98 | } 99 | MetadataType::PrimaryKeyWithPrefix => { 100 | primary_keys_with_prefix = Some(parse_int_map(&mut buffer)?); 101 | } 102 | MetadataType::EnumAndSetDefaultCharset => { 103 | enum_and_set_default_charset = Some(parse_default_charser(&mut buffer)?); 104 | } 105 | MetadataType::EnumAndSetColumnCharset => { 106 | enum_and_set_column_charsets = Some(parse_int_array(&mut buffer)?); 107 | } 108 | MetadataType::ColumnVisibility => { 109 | column_visibility = 110 | Some(read_bitmap_reverted(&mut buffer, column_types.len())?); 111 | } 112 | } 113 | } 114 | 115 | Ok(Self { 116 | signedness, 117 | default_charset, 118 | column_charsets, 119 | column_names, 120 | set_string_values, 121 | enum_string_values, 122 | geometry_types, 123 | simple_primary_keys, 124 | primary_keys_with_prefix, 125 | enum_and_set_default_charset, 126 | enum_and_set_column_charsets, 127 | column_visibility, 128 | }) 129 | } 130 | } 131 | 132 | fn parse_int_array(cursor: &mut Cursor<&[u8]>) -> Result, Error> { 133 | let mut result = Vec::new(); 134 | while cursor.position() < cursor.get_ref().len() as u64 { 135 | let value = read_len_enc_num(cursor)?; 136 | result.push(value as u32); 137 | } 138 | Ok(result) 139 | } 140 | 141 | fn parse_string_array(cursor: &mut Cursor<&[u8]>) -> Result, Error> { 142 | let mut result = Vec::new(); 143 | while cursor.position() < cursor.get_ref().len() as u64 { 144 | let value = read_len_enc_str(cursor)?; 145 | result.push(value); 146 | } 147 | Ok(result) 148 | } 149 | 150 | fn parse_int_map(cursor: &mut Cursor<&[u8]>) -> Result, Error> { 151 | let mut result = Vec::new(); 152 | while cursor.position() < cursor.get_ref().len() as u64 { 153 | let key = read_len_enc_num(cursor)?; 154 | let value = read_len_enc_num(cursor)?; 155 | result.push((key as u32, value as u32)); 156 | } 157 | Ok(result) 158 | } 159 | 160 | fn parse_type_values(cursor: &mut Cursor<&[u8]>) -> Result>, Error> { 161 | let mut result = Vec::new(); 162 | while cursor.position() < cursor.get_ref().len() as u64 { 163 | let length = read_len_enc_num(cursor)?; 164 | let mut type_values = Vec::new(); 165 | for _i in 0..length { 166 | type_values.push(read_len_enc_str(cursor)?); 167 | } 168 | result.push(type_values); 169 | } 170 | Ok(result) 171 | } 172 | 173 | fn parse_default_charser(cursor: &mut Cursor<&[u8]>) -> Result { 174 | let default_collation = read_len_enc_num(cursor)?; 175 | let charset_collations = parse_int_map(cursor)?; 176 | Ok(DefaultCharset::new( 177 | default_collation as u32, 178 | charset_collations, 179 | )) 180 | } 181 | 182 | fn read_bitmap_reverted( 183 | cursor: &mut Cursor<&[u8]>, 184 | bits_number: usize, 185 | ) -> Result, io::Error> { 186 | let mut result = vec![false; bits_number]; 187 | let bytes_number = (bits_number + 7) / 8; 188 | for i in 0..bytes_number { 189 | let value = cursor.read_u8()?; 190 | for y in 0..8 { 191 | let index = (i << 3) + y; 192 | if index == bits_number { 193 | break; 194 | } 195 | 196 | // The difference from ReadBitmap is that bits are reverted 197 | result[index] = (value & (1 << (7 - y))) > 0; 198 | } 199 | } 200 | Ok(result) 201 | } 202 | 203 | fn get_numeric_column_count(column_types: &[u8]) -> Result { 204 | let mut count = 0; 205 | for i in 0..column_types.len() { 206 | match ColumnType::from_code(column_types[i])? { 207 | ColumnType::Tiny => count += 1, 208 | ColumnType::Short => count += 1, 209 | ColumnType::Int24 => count += 1, 210 | ColumnType::Long => count += 1, 211 | ColumnType::LongLong => count += 1, 212 | ColumnType::Float => count += 1, 213 | ColumnType::Double => count += 1, 214 | ColumnType::NewDecimal => count += 1, 215 | _ => (), 216 | } 217 | } 218 | Ok(count) 219 | } 220 | -------------------------------------------------------------------------------- /src/packet_channel.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; 2 | use std::io::{self, Read, Write}; 3 | use std::net::TcpStream; 4 | 5 | use crate::constants::{PACKET_HEADER_SIZE, TIMEOUT_LATENCY_DELTA}; 6 | use crate::replica_options::ReplicaOptions; 7 | 8 | pub struct PacketChannel { 9 | stream: TcpStream, 10 | } 11 | 12 | impl PacketChannel { 13 | pub fn new(options: &ReplicaOptions) -> Result { 14 | let address: String = format!("{}:{}", options.hostname, options.port.to_string()); 15 | let stream = TcpStream::connect(address)?; 16 | let read_timeout = options.heartbeat_interval + TIMEOUT_LATENCY_DELTA; 17 | stream.set_read_timeout(Some(read_timeout))?; 18 | Ok(Self { stream }) 19 | } 20 | 21 | pub fn read_packet(&mut self) -> Result<(Vec, u8), io::Error> { 22 | let mut header_buffer = [0; PACKET_HEADER_SIZE]; 23 | 24 | self.stream.read_exact(&mut header_buffer)?; 25 | let packet_size = (&header_buffer[0..3]).read_u24::()?; 26 | let seq_num = header_buffer[3]; 27 | 28 | let mut packet: Vec = vec![0; packet_size as usize]; 29 | self.stream.read_exact(&mut packet)?; 30 | 31 | Ok((packet, seq_num)) 32 | } 33 | 34 | pub fn write_packet(&mut self, packet: &[u8], seq_num: u8) -> Result<(), io::Error> { 35 | let packet_len = packet.len() as u32; 36 | self.stream.write_u24::(packet_len)?; 37 | self.stream.write_u8(seq_num)?; 38 | self.stream.write(packet)?; 39 | Ok(()) 40 | } 41 | 42 | pub fn upgrade_to_ssl(&mut self) { 43 | unimplemented!(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/providers/mariadb/events/gtid_event.rs: -------------------------------------------------------------------------------- 1 | use crate::providers::mariadb::gtid::gtid::Gtid; 2 | use crate::{errors::Error, events::event_header::EventHeader}; 3 | use byteorder::{LittleEndian, ReadBytesExt}; 4 | use std::io::Cursor; 5 | 6 | /// Marks start of a new event group(transaction). 7 | /// See more 8 | #[derive(Debug)] 9 | pub struct GtidEvent { 10 | /// Gets Global Transaction ID of the event group. 11 | pub gtid: Gtid, 12 | 13 | /// Gets flags. 14 | pub flags: u8, 15 | } 16 | 17 | impl GtidEvent { 18 | /// Parses events in MariaDB 10.0.2+. 19 | pub fn parse(cursor: &mut Cursor<&[u8]>, header: &EventHeader) -> Result { 20 | let sequence = cursor.read_u64::()?; 21 | let domain_id = cursor.read_u32::()?; 22 | let flags = cursor.read_u8()?; 23 | 24 | let gtid = Gtid::new(domain_id, header.server_id, sequence); 25 | Ok(Self { gtid, flags }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/providers/mariadb/events/gtid_list_event.rs: -------------------------------------------------------------------------------- 1 | use crate::providers::mariadb::gtid::gtid_list::GtidList; 2 | use crate::{errors::Error, providers::mariadb::gtid::gtid::Gtid}; 3 | use byteorder::{LittleEndian, ReadBytesExt}; 4 | use std::io::Cursor; 5 | 6 | /// Shows current replication state with list of last gtid for each replication domain. 7 | /// See more 8 | #[derive(Debug)] 9 | pub struct GtidListEvent { 10 | /// Gets a list of Gtid that represents current replication state 11 | pub gtid_list: GtidList, 12 | } 13 | 14 | impl GtidListEvent { 15 | /// Parses events in MariaDB. 16 | pub fn parse(cursor: &mut Cursor<&[u8]>) -> Result { 17 | let gtid_list_len = cursor.read_u32::()?; 18 | 19 | let mut gtid_list = GtidList::new(); 20 | for _i in 0..gtid_list_len { 21 | let domain_id = cursor.read_u32::()?; 22 | let server_id = cursor.read_u32::()?; 23 | let sequence = cursor.read_u64::()?; 24 | 25 | let gtid = Gtid::new(domain_id, server_id, sequence); 26 | gtid_list.gtids.push(gtid); 27 | } 28 | 29 | Ok(Self { gtid_list }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/providers/mariadb/events/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod gtid_event; 2 | pub mod gtid_list_event; 3 | -------------------------------------------------------------------------------- /src/providers/mariadb/gtid/gtid.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | /// MariaDB 10.0.2+ representation of Gtid. 4 | #[derive(Clone, Debug)] 5 | pub struct Gtid { 6 | /// Gets domain identifier in multi-master setup. 7 | pub domain_id: u32, 8 | 9 | /// Gets identifier of the server that generated the event. 10 | pub server_id: u32, 11 | 12 | /// Gets sequence number of the event on the original server. 13 | pub sequence: u64, 14 | } 15 | 16 | impl Gtid { 17 | pub fn new(domain_id: u32, server_id: u32, sequence: u64) -> Self { 18 | Self { 19 | domain_id, 20 | server_id, 21 | sequence, 22 | } 23 | } 24 | } 25 | 26 | impl fmt::Display for Gtid { 27 | /// Returns string representation of Gtid in MariaDB. 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | write!(f, "{}-{}-{}", self.domain_id, self.server_id, self.sequence) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/providers/mariadb/gtid/gtid_list.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | use crate::providers::mariadb::gtid::gtid::Gtid; 3 | use std::collections::HashSet; 4 | use std::fmt; 5 | 6 | /// Represents GtidList from MariaDB. 7 | #[derive(Debug)] 8 | pub struct GtidList { 9 | /// Gets a list of Gtids per each domain. 10 | pub gtids: Vec, 11 | } 12 | 13 | impl GtidList { 14 | pub fn new() -> Self { 15 | Self { gtids: Vec::new() } 16 | } 17 | 18 | /// Parses from string representation. 19 | pub fn parse(value: &str) -> Result { 20 | if value.is_empty() { 21 | return Ok(GtidList::new()); 22 | } 23 | 24 | let value = value.replace("\n", ""); 25 | let gtid_list = value.split(',').map(|x| x.trim()).collect::>(); 26 | 27 | let mut domain_map = HashSet::new(); 28 | let mut gtids = Vec::new(); 29 | 30 | for gtid in gtid_list { 31 | let components = gtid.split('-').collect::>(); 32 | let domain_id: u32 = components[0].parse()?; 33 | let server_id: u32 = components[1].parse()?; 34 | let sequence: u64 = components[2].parse()?; 35 | 36 | if domain_map.contains(&domain_id) { 37 | return Err(Error::String(format!( 38 | "GtidList must consist of unique domain ids" 39 | ))); 40 | } else { 41 | domain_map.insert(domain_id); 42 | } 43 | 44 | gtids.push(Gtid::new(domain_id, server_id, sequence)); 45 | } 46 | 47 | Ok(Self { gtids }) 48 | } 49 | 50 | /// Adds a gtid value to the GtidList. 51 | pub fn add_gtid(&mut self, gtid: Gtid) -> bool { 52 | for i in 0..self.gtids.len() { 53 | if self.gtids[i].domain_id == gtid.domain_id { 54 | self.gtids[i] = gtid; 55 | return false; 56 | } 57 | } 58 | self.gtids.push(gtid); 59 | true 60 | } 61 | } 62 | 63 | impl fmt::Display for GtidList { 64 | /// Returns string representation of the GtidList. 65 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 66 | let str = self 67 | .gtids 68 | .iter() 69 | .map(|x| x.to_string()) 70 | .collect::>() 71 | .join(","); 72 | 73 | write!(f, "{}", str) 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | use super::GtidList; 80 | use crate::providers::mariadb::gtid::gtid::Gtid; 81 | 82 | #[test] 83 | #[should_panic(expected = "GtidList must consist of unique domain ids")] 84 | fn parse_not_unique_domains() { 85 | GtidList::parse("1-1-270, 1-1-271").unwrap(); 86 | } 87 | 88 | #[test] 89 | fn parse_empty_string_returns_empty_gtid_list() { 90 | let empty = String::from(""); 91 | let gtid_list = GtidList::parse(&empty).unwrap(); 92 | 93 | assert_eq!(0, gtid_list.gtids.len()); 94 | assert_eq!(empty, gtid_list.to_string()); 95 | } 96 | 97 | #[test] 98 | fn parse_gtid_lists_returns_multiple_results() { 99 | let gtid_list1 = GtidList::parse("0-1-270").unwrap(); 100 | let gtid_list2 = GtidList::parse("1-2-120,2-3-130").unwrap(); 101 | let gtid_list3 = GtidList::parse("1-2-120, 2-3-130, 3-4-50").unwrap(); 102 | 103 | assert_eq!(1, gtid_list1.gtids.len()); 104 | assert_eq!(2, gtid_list2.gtids.len()); 105 | assert_eq!(3, gtid_list3.gtids.len()); 106 | 107 | assert_eq!(String::from("0-1-270"), gtid_list1.to_string()); 108 | assert_eq!(String::from("1-2-120,2-3-130"), gtid_list2.to_string()); 109 | assert_eq!( 110 | String::from("1-2-120,2-3-130,3-4-50"), 111 | gtid_list3.to_string() 112 | ); 113 | } 114 | 115 | #[test] 116 | fn add_existing_domain_gtid_updated() { 117 | let mut gtid_list = GtidList::parse("0-1-270").unwrap(); 118 | gtid_list.add_gtid(Gtid::new(0, 1, 271)); 119 | 120 | assert_eq!(1, gtid_list.gtids.len()); 121 | assert_eq!(String::from("0-1-271"), gtid_list.to_string()); 122 | } 123 | 124 | #[test] 125 | fn add_new_domain_gtid_added() { 126 | let mut gtid_list = GtidList::parse("0-1-270").unwrap(); 127 | gtid_list.add_gtid(Gtid::new(1, 1, 271)); 128 | 129 | assert_eq!(2, gtid_list.gtids.len()); 130 | assert_eq!(String::from("0-1-270,1-1-271"), gtid_list.to_string()); 131 | } 132 | 133 | #[test] 134 | fn add_multi_domain_gtid_list_merged() { 135 | let mut gtid_list = GtidList::parse("1-2-120,2-3-130,3-4-50").unwrap(); 136 | gtid_list.add_gtid(Gtid::new(2, 4, 250)); 137 | 138 | assert_eq!(3, gtid_list.gtids.len()); 139 | assert_eq!( 140 | String::from("1-2-120,2-4-250,3-4-50"), 141 | gtid_list.to_string() 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/providers/mariadb/gtid/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod gtid; 2 | pub mod gtid_list; 3 | -------------------------------------------------------------------------------- /src/providers/mariadb/mariadb_provider.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::dump_binlog_command::DumpBinlogCommand; 2 | use crate::commands::query_command::QueryCommand; 3 | use crate::commands::register_slave_command::RegisterSlaveCommand; 4 | use crate::errors::Error; 5 | use crate::extensions::check_error_packet; 6 | use crate::packet_channel::PacketChannel; 7 | use crate::replica_options::ReplicaOptions; 8 | use crate::starting_strategy::StartingStrategy; 9 | 10 | pub fn replicate_mariadb( 11 | channel: &mut PacketChannel, 12 | options: &ReplicaOptions, 13 | server_id: u32, 14 | ) -> Result<(), Error> { 15 | let command = QueryCommand::new("SET @mariadb_slave_capability=4".to_string()); 16 | channel.write_packet(&command.serialize()?, 0)?; 17 | let (packet, _) = channel.read_packet()?; 18 | check_error_packet(&packet, "Setting @mariadb_slave_capability error.")?; 19 | 20 | if options.binlog.starting_strategy == StartingStrategy::FromGtid { 21 | if let Some(gtid_list) = &options.binlog.gtid_list { 22 | register_gtid_slave(channel, options.server_id, >id_list.to_string())?; 23 | } else { 24 | return Err(Error::String("GtidList was not specified".to_string())); 25 | } 26 | } 27 | 28 | let command = DumpBinlogCommand::new( 29 | server_id, 30 | options.binlog.filename.clone(), 31 | options.binlog.position, 32 | ); 33 | channel.write_packet(&command.serialize()?, 0)?; 34 | Ok(()) 35 | } 36 | 37 | fn register_gtid_slave( 38 | channel: &mut PacketChannel, 39 | server_id: u32, 40 | gtid_list: &String, 41 | ) -> Result<(), Error> { 42 | let command = QueryCommand::new(format!("SET @slave_connect_state='{}'", gtid_list)); 43 | channel.write_packet(&command.serialize()?, 0)?; 44 | let (packet, _) = channel.read_packet()?; 45 | check_error_packet(&packet, "Setting @slave_connect_state error.")?; 46 | 47 | let command = QueryCommand::new("SET @slave_gtid_strict_mode=0".to_string()); 48 | channel.write_packet(&command.serialize()?, 0)?; 49 | let (packet, _) = channel.read_packet()?; 50 | check_error_packet(&packet, "Setting @slave_gtid_strict_mode error.")?; 51 | 52 | let command = QueryCommand::new("SET @slave_gtid_ignore_duplicates=0".to_string()); 53 | channel.write_packet(&command.serialize()?, 0)?; 54 | let (packet, _) = channel.read_packet()?; 55 | check_error_packet(&packet, "Setting @slave_gtid_ignore_duplicates error.")?; 56 | 57 | let command = RegisterSlaveCommand::new(server_id); 58 | channel.write_packet(&command.serialize()?, 0)?; 59 | let (packet, _) = channel.read_packet()?; 60 | check_error_packet(&packet, "Registering slave error.")?; 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /src/providers/mariadb/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod events; 2 | pub mod gtid; 3 | pub mod mariadb_provider; 4 | -------------------------------------------------------------------------------- /src/providers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod mariadb; 2 | pub mod mysql; 3 | -------------------------------------------------------------------------------- /src/providers/mysql/events/gtid_event.rs: -------------------------------------------------------------------------------- 1 | use crate::providers::mysql::gtid::uuid::Uuid; 2 | use crate::{errors::Error, providers::mysql::gtid::gtid::Gtid}; 3 | use byteorder::{LittleEndian, ReadBytesExt}; 4 | use std::io::{Cursor, Read}; 5 | 6 | /// Marks start of a new event group(transaction). 7 | /// See more 8 | #[derive(Debug)] 9 | pub struct GtidEvent { 10 | /// Gets Global Transaction ID of the event group. 11 | pub gtid: Gtid, 12 | 13 | /// Gets flags. 14 | pub flags: u8, 15 | } 16 | 17 | impl GtidEvent { 18 | /// Parses events in MySQL 5.6+. 19 | pub fn parse(cursor: &mut Cursor<&[u8]>) -> Result { 20 | let flags = cursor.read_u8()?; 21 | 22 | let mut source_id = [0u8; 16]; 23 | cursor.read_exact(&mut source_id)?; 24 | let source_id = Uuid::new(source_id); 25 | 26 | let transaction_id = cursor.read_u64::()?; 27 | 28 | let gtid = Gtid::new(source_id, transaction_id); 29 | Ok(Self { gtid, flags }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/providers/mysql/events/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod gtid_event; 2 | pub mod prev_gtids_event; 3 | -------------------------------------------------------------------------------- /src/providers/mysql/events/prev_gtids_event.rs: -------------------------------------------------------------------------------- 1 | use crate::providers::mysql::gtid::interval::Interval; 2 | use crate::providers::mysql::gtid::uuid::Uuid; 3 | use crate::providers::mysql::gtid::uuid_set::UuidSet; 4 | use crate::{errors::Error, providers::mysql::gtid::gtid_set::GtidSet}; 5 | use byteorder::{LittleEndian, ReadBytesExt}; 6 | use std::io::{Cursor, Read}; 7 | 8 | /// Used to record the gtid_executed of previous binlog files. 9 | #[derive(Debug)] 10 | pub struct PreviousGtidsEvent { 11 | /// Gets GtidSet of previous files. 12 | pub gtid_set: GtidSet, 13 | } 14 | 15 | impl PreviousGtidsEvent { 16 | /// Parses events in MySQL 5.6+. 17 | pub fn parse(cursor: &mut Cursor<&[u8]>) -> Result { 18 | let uuid_set_number = cursor.read_u64::()?; 19 | let mut gtid_set = GtidSet::new(); 20 | 21 | for _i in 0..uuid_set_number { 22 | let mut source_id = [0u8; 16]; 23 | cursor.read_exact(&mut source_id)?; 24 | let source_id = Uuid::new(source_id); 25 | 26 | let mut uuid_set = UuidSet::new(source_id, Vec::new()); 27 | let interval_number = cursor.read_u64::()?; 28 | for _y in 0..interval_number { 29 | let start = cursor.read_u64::()?; 30 | let end = cursor.read_u64::()?; 31 | uuid_set.intervals.push(Interval::new(start, end - 1)); 32 | } 33 | gtid_set 34 | .uuid_sets 35 | .insert(uuid_set.source_id.uuid.clone(), uuid_set); 36 | } 37 | 38 | Ok(Self { gtid_set }) 39 | } 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use crate::providers::mysql::events::prev_gtids_event::PreviousGtidsEvent; 45 | use std::io::Cursor; 46 | 47 | #[test] 48 | fn parse_event_returns_gtid_set() { 49 | let payload: Vec = vec![ 50 | 2, 0, 0, 0, 0, 0, 0, 0, 181, 205, 22, 36, 95, 48, 17, 228, 180, 233, 16, 81, 114, 27, 51 | 210, 65, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 241, 15, 108, 0, 0, 0, 0, 0, 52 | 187, 66, 29, 38, 95, 48, 17, 228, 180, 233, 216, 157, 103, 43, 46, 248, 1, 0, 0, 0, 0, 53 | 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 209, 97, 119, 0, 0, 0, 0, 0, 54 | ]; 55 | let mut cursor = Cursor::new(payload.as_slice()); 56 | 57 | let expected = String::from("b5cd1624-5f30-11e4-b4e9-1051721bd241:1-7081968,bb421d26-5f30-11e4-b4e9-d89d672b2ef8:1-7823824"); 58 | let event = PreviousGtidsEvent::parse(&mut cursor).unwrap(); 59 | assert_eq!(expected, event.gtid_set.to_string()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/providers/mysql/gtid/gtid.rs: -------------------------------------------------------------------------------- 1 | use crate::providers::mysql::gtid::uuid::Uuid; 2 | use std::fmt; 3 | 4 | /// MySQL 5.6+ representation of Gtid. 5 | #[derive(Clone, Debug)] 6 | pub struct Gtid { 7 | /// Gets identifier of the original server that generated the event. 8 | pub source_id: Uuid, 9 | 10 | /// Gets sequence number of the event on the original server. 11 | pub transaction_id: u64, 12 | } 13 | 14 | impl Gtid { 15 | pub fn new(source_id: Uuid, transaction_id: u64) -> Self { 16 | Self { 17 | source_id, 18 | transaction_id, 19 | } 20 | } 21 | } 22 | 23 | impl fmt::Display for Gtid { 24 | /// Returns string representation of Gtid in MySQL Server. 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | write!(f, "{}:{}", self.source_id, self.transaction_id) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/providers/mysql/gtid/gtid_set.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | use crate::providers::mysql::gtid::gtid::Gtid; 3 | use crate::providers::mysql::gtid::interval::Interval; 4 | use crate::providers::mysql::gtid::uuid::Uuid; 5 | use crate::providers::mysql::gtid::uuid_set::UuidSet; 6 | use std::collections::HashMap; 7 | use std::fmt; 8 | 9 | const UUID_LENGTH: usize = 36; 10 | 11 | /// Represents GtidSet from MySQL 5.6 and above. 12 | /// See more 13 | #[derive(Debug)] 14 | pub struct GtidSet { 15 | /// Gets a list of UuidSet parts in the GtidSet. 16 | pub uuid_sets: HashMap, 17 | } 18 | 19 | impl GtidSet { 20 | pub fn new() -> Self { 21 | Self { 22 | uuid_sets: HashMap::new(), 23 | } 24 | } 25 | 26 | /// Parses from string representation. 27 | pub fn parse(gtid_set: &str) -> Result { 28 | if gtid_set.is_empty() { 29 | return Ok(GtidSet::new()); 30 | } 31 | 32 | let gtid_set = gtid_set.replace("\n", ""); 33 | let uuid_sets = gtid_set.split(',').map(|x| x.trim()).collect::>(); 34 | 35 | let mut result = HashMap::new(); 36 | for uuid_set in uuid_sets { 37 | let source_id: String = uuid_set.chars().take(UUID_LENGTH).collect(); 38 | let source_id = Uuid::parse(source_id)?; 39 | 40 | let mut intervals = Vec::new(); 41 | let ranges: String = uuid_set.chars().skip(UUID_LENGTH + 1).collect(); 42 | let ranges = ranges.split(':').collect::>(); 43 | 44 | for token in ranges { 45 | let range = token.split('-').collect::>(); 46 | let interval = match range.len() { 47 | 1 => Interval::new(range[0].parse()?, range[0].parse()?), 48 | 2 => Interval::new(range[0].parse()?, range[1].parse()?), 49 | _ => return Err(Error::String(format!("Invalid interval format {}", token))), 50 | }; 51 | intervals.push(interval); 52 | } 53 | result.insert(source_id.uuid.clone(), UuidSet::new(source_id, intervals)); 54 | } 55 | 56 | Ok(Self { uuid_sets: result }) 57 | } 58 | 59 | /// Adds a gtid value to the GtidSet. 60 | pub fn add_gtid(&mut self, gtid: Gtid) -> Result { 61 | let uuid_set = self 62 | .uuid_sets 63 | .entry(gtid.source_id.uuid.clone()) 64 | .or_insert(UuidSet::new(gtid.source_id.clone(), Vec::new())); 65 | 66 | Ok(uuid_set.add_gtid(gtid)?) 67 | } 68 | } 69 | 70 | impl fmt::Display for GtidSet { 71 | /// Returns string representation of the GtidSet. 72 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 73 | let mut uuids = self 74 | .uuid_sets 75 | .values() 76 | .map(|x| x.to_string()) 77 | .collect::>(); 78 | 79 | uuids.sort(); 80 | write!(f, "{}", uuids.join(",")) 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use crate::providers::mysql::gtid::{ 87 | gtid::Gtid, gtid_set::GtidSet, interval::Interval, uuid::Uuid, 88 | }; 89 | 90 | pub const SERVER_UUID1: &str = "24bc7850-2c16-11e6-a073-0242ac110001"; 91 | pub const SERVER_UUID2: &str = "24bc7850-2c16-11e6-a073-0242ac110002"; 92 | 93 | fn create_uuid1() -> Uuid { 94 | Uuid::parse(String::from(SERVER_UUID1)).unwrap() 95 | } 96 | 97 | fn create_uuid2() -> Uuid { 98 | Uuid::parse(String::from(SERVER_UUID2)).unwrap() 99 | } 100 | 101 | #[test] 102 | fn parse_empty_string_returns_empty_gtid_set() { 103 | let empty = String::from(""); 104 | let gtid_set = GtidSet::parse(&empty).unwrap(); 105 | 106 | assert_eq!(0, gtid_set.uuid_sets.len()); 107 | assert_eq!(empty, gtid_set.to_string()); 108 | } 109 | 110 | #[test] 111 | fn add_gtids_lists_merged() { 112 | let mut gtid_set = GtidSet::parse(&format!("{}:3-5", SERVER_UUID1)).unwrap(); 113 | 114 | gtid_set.add_gtid(Gtid::new(create_uuid1(), 2)).unwrap(); 115 | gtid_set.add_gtid(Gtid::new(create_uuid1(), 4)).unwrap(); 116 | gtid_set.add_gtid(Gtid::new(create_uuid1(), 5)).unwrap(); 117 | gtid_set.add_gtid(Gtid::new(create_uuid1(), 7)).unwrap(); 118 | gtid_set.add_gtid(Gtid::new(create_uuid2(), 9)).unwrap(); 119 | gtid_set.add_gtid(Gtid::new(create_uuid1(), 0)).unwrap(); 120 | 121 | assert_eq!( 122 | format!("{}:0:2-5:7,{}:9", SERVER_UUID1, SERVER_UUID2), 123 | gtid_set.to_string() 124 | ); 125 | } 126 | 127 | #[test] 128 | fn add_gtid_in_gap_intervals_joined() { 129 | let mut gtid_set = GtidSet::parse(&format!("{}:3-4:6-7", SERVER_UUID1)).unwrap(); 130 | 131 | gtid_set.add_gtid(Gtid::new(create_uuid1(), 5)).unwrap(); 132 | 133 | assert_eq!(format!("{}:3-7", SERVER_UUID1), gtid_set.to_string()); 134 | } 135 | 136 | #[test] 137 | fn raw_gtid_sets_equals_correctly() { 138 | let list_1 = GtidSet::parse(&format!("{}:1-191:192-199", SERVER_UUID1)).unwrap(); 139 | let list_2 = GtidSet::parse(&format!("{}:1-199", SERVER_UUID1)).unwrap(); 140 | assert_eq!(list_1.to_string(), list_2.to_string()); 141 | 142 | let list_1 = GtidSet::parse(&format!("{}:1-191:193-199", SERVER_UUID1)).unwrap(); 143 | let list_2 = GtidSet::parse(&format!("{}:1-199", SERVER_UUID1)).unwrap(); 144 | assert_ne!(list_1.to_string(), list_2.to_string()); 145 | } 146 | 147 | #[test] 148 | fn simple_gtid_set_has_single_interval() { 149 | let gtid_set = GtidSet::parse(&format!("{}:1-191", SERVER_UUID1)).unwrap(); 150 | let uuid_set = gtid_set.uuid_sets.get(&create_uuid1().to_string()).unwrap(); 151 | 152 | assert_eq!(1, uuid_set.intervals.len()); 153 | assert_eq!( 154 | Interval::new(1, 191).to_string(), 155 | uuid_set.intervals[0].to_string() 156 | ); 157 | assert_eq!(format!("{}:1-191", SERVER_UUID1), gtid_set.to_string()); 158 | } 159 | 160 | #[test] 161 | fn continuous_intervals_collapsed() { 162 | let gtid_set = GtidSet::parse(&format!("{}:1-191:192-199", SERVER_UUID1)).unwrap(); 163 | let uuid_set = gtid_set.uuid_sets.get(&create_uuid1().to_string()).unwrap(); 164 | 165 | assert_eq!(1, uuid_set.intervals.len()); 166 | assert_eq!( 167 | Interval::new(1, 199).to_string(), 168 | uuid_set.intervals[0].to_string() 169 | ); 170 | assert_eq!(format!("{}:1-199", SERVER_UUID1), gtid_set.to_string()); 171 | } 172 | 173 | #[test] 174 | fn non_continuous_intervals_not_collapsed() { 175 | let gtid_set = GtidSet::parse(&format!("{}:1-191:193-199", SERVER_UUID1)).unwrap(); 176 | let uuid_set = gtid_set.uuid_sets.get(&create_uuid1().to_string()).unwrap(); 177 | 178 | assert_eq!(2, uuid_set.intervals.len()); 179 | assert_eq!( 180 | Interval::new(1, 191).to_string(), 181 | uuid_set.intervals[0].to_string() 182 | ); 183 | assert_eq!( 184 | Interval::new(193, 199).to_string(), 185 | uuid_set.intervals[1].to_string() 186 | ); 187 | assert_eq!( 188 | format!("{}:1-191:193-199", SERVER_UUID1), 189 | gtid_set.to_string() 190 | ); 191 | } 192 | 193 | #[test] 194 | fn multiple_intervals_not_collapsed() { 195 | let gtid_set = 196 | GtidSet::parse(&format!("{}:1-191:193-199:1000-1033", SERVER_UUID1)).unwrap(); 197 | let uuid_set = gtid_set.uuid_sets.get(&create_uuid1().to_string()).unwrap(); 198 | 199 | assert_eq!(3, uuid_set.intervals.len()); 200 | assert_eq!( 201 | Interval::new(1, 191).to_string(), 202 | uuid_set.intervals[0].to_string() 203 | ); 204 | assert_eq!( 205 | Interval::new(193, 199).to_string(), 206 | uuid_set.intervals[1].to_string() 207 | ); 208 | assert_eq!( 209 | Interval::new(1000, 1033).to_string(), 210 | uuid_set.intervals[2].to_string() 211 | ); 212 | assert_eq!( 213 | format!("{}:1-191:193-199:1000-1033", SERVER_UUID1), 214 | gtid_set.to_string() 215 | ); 216 | } 217 | 218 | #[test] 219 | fn multiple_intervals_some_collapsed() { 220 | let gtid_set = GtidSet::parse(&format!( 221 | "{}:1-191:192-199:1000-1033:1035-1036:1038-1039", 222 | SERVER_UUID1 223 | )) 224 | .unwrap(); 225 | let uuid_set = gtid_set.uuid_sets.get(&create_uuid1().to_string()).unwrap(); 226 | 227 | assert_eq!(4, uuid_set.intervals.len()); 228 | assert_eq!( 229 | Interval::new(1, 199).to_string(), 230 | uuid_set.intervals[0].to_string() 231 | ); 232 | assert_eq!( 233 | Interval::new(1000, 1033).to_string(), 234 | uuid_set.intervals[1].to_string() 235 | ); 236 | assert_eq!( 237 | Interval::new(1035, 1036).to_string(), 238 | uuid_set.intervals[2].to_string() 239 | ); 240 | assert_eq!( 241 | Interval::new(1038, 1039).to_string(), 242 | uuid_set.intervals[3].to_string() 243 | ); 244 | assert_eq!( 245 | format!("{}:1-199:1000-1033:1035-1036:1038-1039", SERVER_UUID1), 246 | gtid_set.to_string() 247 | ); 248 | } 249 | 250 | #[test] 251 | fn multi_server_setup_has_single_intervals_trims_spaces() { 252 | let gtid_set = GtidSet::parse(&format!( 253 | "{}:1-3:11:47-49, {}:1-19:55:56-100", 254 | SERVER_UUID1, SERVER_UUID2 255 | )) 256 | .unwrap(); 257 | 258 | assert_eq!(2, gtid_set.uuid_sets.len()); 259 | 260 | let uuid_set_1 = gtid_set.uuid_sets.get(&create_uuid1().to_string()).unwrap(); 261 | let uuid_set_2 = gtid_set.uuid_sets.get(&create_uuid2().to_string()).unwrap(); 262 | 263 | assert_eq!(3, uuid_set_1.intervals.len()); 264 | assert_eq!( 265 | Interval::new(1, 3).to_string(), 266 | uuid_set_1.intervals[0].to_string() 267 | ); 268 | assert_eq!( 269 | Interval::new(11, 11).to_string(), 270 | uuid_set_1.intervals[1].to_string() 271 | ); 272 | assert_eq!( 273 | Interval::new(47, 49).to_string(), 274 | uuid_set_1.intervals[2].to_string() 275 | ); 276 | 277 | assert_eq!(2, uuid_set_2.intervals.len()); 278 | assert_eq!( 279 | Interval::new(1, 19).to_string(), 280 | uuid_set_2.intervals[0].to_string() 281 | ); 282 | assert_eq!( 283 | Interval::new(55, 100).to_string(), 284 | uuid_set_2.intervals[1].to_string() 285 | ); 286 | 287 | assert_eq!( 288 | format!("{}:1-3:11:47-49,{}:1-19:55-100", SERVER_UUID1, SERVER_UUID2), 289 | gtid_set.to_string() 290 | ); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/providers/mysql/gtid/interval.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | /// Represents contiguous transaction interval in GtidSet. 4 | #[derive(Debug)] 5 | pub struct Interval { 6 | /// Gets first transaction id in the interval. 7 | pub start: u64, 8 | 9 | /// Gets last transaction id in the interval. 10 | pub end: u64, 11 | } 12 | 13 | impl Interval { 14 | pub fn new(start: u64, end: u64) -> Self { 15 | Self { start, end } 16 | } 17 | } 18 | 19 | impl fmt::Display for Interval { 20 | /// Returns string representation of an UuidSet interval. 21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 22 | if self.start == self.end { 23 | write!(f, "{}", self.start) 24 | } else { 25 | write!(f, "{}-{}", self.start, self.end) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/providers/mysql/gtid/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod gtid; 2 | pub mod gtid_set; 3 | pub mod interval; 4 | pub mod uuid; 5 | pub mod uuid_set; 6 | -------------------------------------------------------------------------------- /src/providers/mysql/gtid/uuid.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::errors::Error; 4 | 5 | /// Represents Uuid with little-endian bytes order unlike big-endian Guid. 6 | #[derive(Clone, Debug)] 7 | pub struct Uuid { 8 | pub data: [u8; 16], 9 | pub uuid: String, 10 | } 11 | 12 | impl Uuid { 13 | pub fn new(data: [u8; 16]) -> Self { 14 | let mut uuid = hex::encode(data); 15 | uuid.insert(20, '-'); 16 | uuid.insert(16, '-'); 17 | uuid.insert(12, '-'); 18 | uuid.insert(8, '-'); 19 | Self { data, uuid } 20 | } 21 | 22 | /// Parses Uuid from string representation. 23 | pub fn parse(uuid: String) -> Result { 24 | let hex = uuid.replace("-", ""); 25 | let vec = hex::decode(hex)?; 26 | 27 | let mut data = [0u8; 16]; 28 | (0..16).for_each(|i| data[i] = vec[i]); 29 | 30 | Ok(Self { data, uuid }) 31 | } 32 | } 33 | 34 | impl fmt::Display for Uuid { 35 | /// Returns string representation of the UUID. 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | write!(f, "{}", self.uuid) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/providers/mysql/gtid/uuid_set.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | use crate::providers::mysql::gtid::gtid::Gtid; 3 | use crate::providers::mysql::gtid::interval::Interval; 4 | use crate::providers::mysql::gtid::uuid::Uuid; 5 | use std::fmt; 6 | 7 | /// Represents replication state for a specific server. 8 | #[derive(Debug)] 9 | pub struct UuidSet { 10 | /// Gets server uuid of the UuidSet. 11 | pub source_id: Uuid, 12 | 13 | /// Gets a list of intervals of the UuidSet. 14 | pub intervals: Vec, 15 | } 16 | 17 | impl UuidSet { 18 | pub fn new(source_id: Uuid, mut intervals: Vec) -> Self { 19 | if intervals.len() > 1 { 20 | collapse_intervals(&mut intervals); 21 | } 22 | Self { 23 | source_id, 24 | intervals, 25 | } 26 | } 27 | 28 | /// Adds a gtid value to the UuidSet. 29 | pub fn add_gtid(&mut self, gtid: Gtid) -> Result { 30 | if self.source_id.data != gtid.source_id.data { 31 | return Err(Error::String( 32 | "SourceId of the passed gtid doesn't belong to the UuidSet".to_string(), 33 | )); 34 | } 35 | 36 | let index = find_interval_index(&self.intervals, gtid.transaction_id); 37 | let mut added = false; 38 | if index < self.intervals.len() { 39 | let interval = &mut self.intervals[index]; 40 | if interval.start == gtid.transaction_id + 1 { 41 | interval.start = gtid.transaction_id; 42 | added = true; 43 | } else if interval.end + 1 == gtid.transaction_id { 44 | interval.end = gtid.transaction_id; 45 | added = true; 46 | } else if interval.start <= gtid.transaction_id && gtid.transaction_id <= interval.end { 47 | return Ok(false); 48 | } 49 | } 50 | if !added { 51 | let interval = Interval::new(gtid.transaction_id, gtid.transaction_id); 52 | self.intervals.insert(index, interval); 53 | } 54 | if self.intervals.len() > 1 { 55 | collapse_intervals(&mut self.intervals); 56 | } 57 | Ok(true) 58 | } 59 | } 60 | 61 | pub fn find_interval_index(intervals: &Vec, transaction_id: u64) -> usize { 62 | let mut result_index = 0; 63 | let mut left_index = 0; 64 | let mut right_index = intervals.len(); 65 | 66 | while left_index < right_index { 67 | result_index = (left_index + right_index) / 2; 68 | let interval = &intervals[result_index]; 69 | if interval.end < transaction_id { 70 | left_index = result_index + 1; 71 | } else if transaction_id < interval.start { 72 | right_index = result_index; 73 | } else { 74 | return result_index; 75 | } 76 | } 77 | if !intervals.is_empty() && intervals[result_index].end < transaction_id { 78 | result_index += 1; 79 | } 80 | result_index 81 | } 82 | 83 | pub fn collapse_intervals(intervals: &mut Vec) { 84 | let mut index = 0; 85 | 86 | while index < intervals.len() - 1 { 87 | let right_start = intervals[index + 1].start; 88 | let right_end = intervals[index + 1].end; 89 | 90 | let mut left = &mut intervals[index]; 91 | if left.end + 1 == right_start { 92 | left.end = right_end; 93 | intervals.remove(index + 1); 94 | } else { 95 | index += 1; 96 | } 97 | } 98 | } 99 | 100 | impl fmt::Display for UuidSet { 101 | /// Returns string representation of an UuidSet part of a GtidSet. 102 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 103 | let intervals = self 104 | .intervals 105 | .iter() 106 | .map(|x| x.to_string()) 107 | .collect::>() 108 | .join(":"); 109 | 110 | write!(f, "{}:{}", self.source_id, intervals) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/providers/mysql/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod events; 2 | pub mod gtid; 3 | pub mod mysql_provider; 4 | -------------------------------------------------------------------------------- /src/providers/mysql/mysql_provider.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::dump_binlog_command::DumpBinlogCommand; 2 | use crate::commands::dump_binlog_gtid_command::DumpBinlogGtidCommand; 3 | use crate::errors::Error; 4 | use crate::packet_channel::PacketChannel; 5 | use crate::replica_options::ReplicaOptions; 6 | use crate::starting_strategy::StartingStrategy; 7 | 8 | pub fn replicate_mysql( 9 | channel: &mut PacketChannel, 10 | options: &ReplicaOptions, 11 | server_id: u32, 12 | ) -> Result<(), Error> { 13 | if options.binlog.starting_strategy == StartingStrategy::FromGtid { 14 | if let Some(gtid_set) = &options.binlog.gtid_set { 15 | let command = DumpBinlogGtidCommand::new( 16 | server_id, 17 | options.binlog.filename.clone(), 18 | options.binlog.position, 19 | ); 20 | channel.write_packet(&command.serialize(>id_set)?, 0)? 21 | } else { 22 | return Err(Error::String("GtidSet was not specified".to_string())); 23 | } 24 | } else { 25 | let command = DumpBinlogCommand::new( 26 | server_id, 27 | options.binlog.filename.clone(), 28 | options.binlog.position, 29 | ); 30 | channel.write_packet(&command.serialize()?, 0)? 31 | } 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /src/replica_options.rs: -------------------------------------------------------------------------------- 1 | use crate::binlog_options::BinlogOptions; 2 | use crate::ssl_mode::SslMode; 3 | use std::time::Duration; 4 | 5 | /// Settings used to connect to MySQL/MariaDB. 6 | pub struct ReplicaOptions { 7 | /// Port number to connect. Defaults to 3306. 8 | pub port: u16, 9 | 10 | /// Hostname to connect. Defaults to "localhost". 11 | pub hostname: String, 12 | 13 | /// Defines whether SSL/TLS must be used. Defaults to SslMode.DISABLED. 14 | pub ssl_mode: SslMode, 15 | 16 | /// A database user which is used to register as a database slave. 17 | /// The user needs to have REPLICATION SLAVE, REPLICATION CLIENT privileges. 18 | pub username: String, 19 | 20 | /// The password of the user which is used to connect. 21 | pub password: String, 22 | 23 | /// Default database name specified in Handshake connection. 24 | /// Has nothing to do with filtering events by database name. 25 | pub database: Option, 26 | 27 | /// Specifies the slave server id and used only in blocking mode. Defaults to 65535. 28 | /// See more 29 | pub server_id: u32, 30 | 31 | /// Specifies whether to stream events or read until last event and then return. 32 | /// Defaults to true (stream events and wait for new ones). 33 | pub blocking: bool, 34 | 35 | /// Defines interval of keep alive messages that the master sends to the slave. 36 | /// Defaults to 30 seconds. 37 | pub heartbeat_interval: Duration, 38 | 39 | /// Defines the binlog coordinates that replication should start from. 40 | /// Defaults to BinlogOptions.FromEnd() 41 | pub binlog: BinlogOptions, 42 | } 43 | 44 | impl Default for ReplicaOptions { 45 | fn default() -> ReplicaOptions { 46 | ReplicaOptions { 47 | port: 3306, 48 | hostname: String::from("localhost"), 49 | ssl_mode: SslMode::Disabled, 50 | username: String::new(), 51 | password: String::new(), 52 | database: None, 53 | server_id: 65535, 54 | blocking: true, 55 | heartbeat_interval: Duration::from_secs(30), 56 | binlog: BinlogOptions::from_end(), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/responses/auth_switch_packet.rs: -------------------------------------------------------------------------------- 1 | use crate::{errors::Error, extensions::read_null_term_string}; 2 | use std::io::Cursor; 3 | 4 | /// Authentication Switch Request. 5 | /// See more 6 | #[derive(Debug)] 7 | pub struct AuthPluginSwitchPacket { 8 | pub auth_plugin_name: String, 9 | pub auth_plugin_data: String, 10 | } 11 | 12 | impl AuthPluginSwitchPacket { 13 | pub fn parse(packet: &[u8]) -> Result { 14 | let mut cursor = Cursor::new(packet); 15 | 16 | let auth_plugin_name = read_null_term_string(&mut cursor)?; 17 | let auth_plugin_data = read_null_term_string(&mut cursor)?; 18 | 19 | Ok(Self { 20 | auth_plugin_name, 21 | auth_plugin_data, 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/responses/end_of_file_packet.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, ReadBytesExt}; 2 | use std::io::{self, Cursor}; 3 | 4 | /// EOF packet marks the end of a resultset and returns status and warnings. 5 | /// See more 6 | #[derive(Debug)] 7 | pub struct EndOfFilePacket { 8 | pub warning_count: u16, 9 | pub server_status: u16, 10 | } 11 | 12 | impl EndOfFilePacket { 13 | pub fn parse(packet: &[u8]) -> Result { 14 | let mut cursor = Cursor::new(packet); 15 | 16 | let warning_count = cursor.read_u16::()?; 17 | let server_status = cursor.read_u16::()?; 18 | 19 | Ok(Self { 20 | warning_count, 21 | server_status, 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/responses/error_packet.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, ReadBytesExt}; 2 | use std::io::{self, Cursor, Read}; 3 | 4 | /// ERR_Packet indicates that an error occured. 5 | /// See more 6 | #[derive(Debug)] 7 | pub struct ErrorPacket { 8 | pub error_code: u16, 9 | pub error_message: String, 10 | pub sql_state: Option, 11 | } 12 | 13 | impl ErrorPacket { 14 | pub fn parse(packet: &[u8]) -> Result { 15 | let mut cursor = Cursor::new(packet); 16 | 17 | let error_code = cursor.read_u16::()?; 18 | 19 | let mut error_message = String::new(); 20 | cursor.read_to_string(&mut error_message)?; 21 | 22 | let mut sql_state = None; 23 | if error_message.starts_with('#') { 24 | sql_state = Some(error_message.chars().skip(1).take(5).collect()); 25 | error_message = error_message.chars().skip(6).collect(); 26 | } 27 | 28 | Ok(Self { 29 | error_code, 30 | error_message, 31 | sql_state, 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/responses/handshake_packet.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::capability_flags; 2 | use crate::errors::Error; 3 | use crate::extensions::{read_null_term_string, read_string}; 4 | use byteorder::{LittleEndian, ReadBytesExt}; 5 | use std::io::{Cursor, Read}; 6 | 7 | /// Initial handshake packet sent by the server. 8 | /// See more 9 | #[derive(Debug)] 10 | pub struct HandshakePacket { 11 | pub protocol_version: u8, 12 | pub server_version: String, 13 | pub connection_id: u32, 14 | pub scramble: String, 15 | pub server_capabilities: u64, 16 | pub server_collation: u8, 17 | pub status_flags: u16, 18 | pub filler: String, 19 | pub auth_plugin_length: u8, 20 | pub auth_plugin_name: String, 21 | } 22 | 23 | impl HandshakePacket { 24 | pub fn parse(packet: &[u8]) -> Result { 25 | let mut cursor = Cursor::new(packet); 26 | 27 | let protocol_version = cursor.read_u8()?; 28 | let server_version = read_null_term_string(&mut cursor)?; 29 | let connection_id = cursor.read_u32::()?; 30 | let mut scramble = read_null_term_string(&mut cursor)?; 31 | 32 | let mut capability_flags_1 = vec![0u8; 2]; 33 | cursor.read_exact(&mut capability_flags_1)?; 34 | 35 | let server_collation = cursor.read_u8()?; 36 | let status_flags = cursor.read_u16::()?; 37 | 38 | let mut capability_flags_2 = vec![0u8; 2]; 39 | cursor.read_exact(&mut capability_flags_2)?; 40 | 41 | let auth_plugin_length = cursor.read_u8()?; 42 | let filler = read_string(&mut cursor, 6)?; 43 | 44 | let mut capability_flags_3 = vec![0u8; 4]; 45 | cursor.read_exact(&mut capability_flags_3)?; 46 | 47 | // Join lower and upper capability flags to a number 48 | let capability_flags = 49 | [capability_flags_1, capability_flags_2, capability_flags_3].concat(); 50 | 51 | let server_capabilities = Cursor::new(&capability_flags).read_u64::()?; 52 | 53 | // Handle specific conditions 54 | if (server_capabilities & capability_flags::SECURE_CONNECTION) > 0 { 55 | scramble += &read_null_term_string(&mut cursor)?; 56 | } 57 | 58 | let mut auth_plugin_name = String::new(); 59 | if (server_capabilities & capability_flags::PLUGIN_AUTH) > 0 { 60 | auth_plugin_name = read_null_term_string(&mut cursor)?; 61 | } 62 | 63 | Ok(Self { 64 | protocol_version, 65 | server_version, 66 | connection_id, 67 | scramble, 68 | server_capabilities, 69 | server_collation, 70 | status_flags, 71 | filler, 72 | auth_plugin_length, 73 | auth_plugin_name, 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/responses/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth_switch_packet; 2 | pub mod end_of_file_packet; 3 | pub mod error_packet; 4 | pub mod handshake_packet; 5 | pub mod response_type; 6 | pub mod result_set_row_packet; 7 | -------------------------------------------------------------------------------- /src/responses/response_type.rs: -------------------------------------------------------------------------------- 1 | pub mod ResponseType { 2 | pub const OK: u8 = 0x00; 3 | pub const ERROR: u8 = 0xFF; 4 | pub const END_OF_FILE: u8 = 0xFE; 5 | pub const AUTH_PLUGIN_SWITCH: u8 = 0xFE; 6 | } 7 | -------------------------------------------------------------------------------- /src/responses/result_set_row_packet.rs: -------------------------------------------------------------------------------- 1 | use crate::{errors::Error, extensions::read_len_enc_str}; 2 | use std::io::Cursor; 3 | 4 | /// Returned in response to a QueryCommand. 5 | /// See more 6 | #[derive(Debug)] 7 | pub struct ResultSetRowPacket { 8 | pub cells: Vec, 9 | } 10 | 11 | impl ResultSetRowPacket { 12 | pub fn parse(packet: &[u8]) -> Result { 13 | let mut cursor = Cursor::new(packet); 14 | 15 | let len = cursor.get_ref().len() as u64; 16 | let mut cells = Vec::new(); 17 | 18 | while cursor.position() < len { 19 | cells.push(read_len_enc_str(&mut cursor)?); 20 | } 21 | 22 | Ok(Self { cells }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ssl_mode.rs: -------------------------------------------------------------------------------- 1 | /// Represents the SSL strategy used to connect to the server. 2 | #[derive(Clone, Copy, PartialEq, Debug)] 3 | pub enum SslMode { 4 | /// Establishes an unencrypted connection. 5 | Disabled = 0, 6 | 7 | /// Tries to establish an encrypted connection without verifying CA/Host. Falls back to an unencrypted connection. 8 | IfAvailable = 1, 9 | 10 | /// Require an encrypted connection without verifying CA/Host. 11 | Require = 2, 12 | 13 | /// Verify that the certificate belongs to the Certificate Authority. 14 | RequireVerifyCa = 3, 15 | 16 | /// Verify that the certificate belongs to the Certificate Authority and matches Host. 17 | RequireVerifyFull = 4, 18 | } 19 | -------------------------------------------------------------------------------- /src/starting_strategy.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, PartialEq, Debug)] 2 | pub enum StartingStrategy { 3 | FromStart, 4 | FromEnd, 5 | FromPosition, 6 | FromGtid, 7 | } 8 | --------------------------------------------------------------------------------