From 65207f96c8bc75900b845445df36557e49848d19 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Tue, 26 Dec 2023 23:08:06 +0800 Subject: [PATCH] Create custom cxx binding to Utf8CStr --- native/src/base/cstr.rs | 20 +++++++++++++- native/src/base/cxx_extern.rs | 26 ++++++++++++++----- native/src/base/files.cpp | 2 +- native/src/base/lib.rs | 9 +++++-- native/src/base/logging.cpp | 2 +- native/src/base/logging.rs | 4 +-- native/src/base/misc.cpp | 16 ++++++++++++ native/src/base/misc.hpp | 27 +++++++++++++++++++ native/src/core/daemon.rs | 14 +++------- native/src/core/include/daemon.hpp | 3 +++ native/src/core/lib.rs | 15 +++++++---- native/src/core/logging.rs | 3 ++- native/src/core/package.cpp | 4 +-- native/src/core/resetprop/persist.rs | 17 +++++------- native/src/sepolicy/api.cpp | 2 +- native/src/sepolicy/include/sepolicy.hpp | 5 ++-- native/src/sepolicy/lib.rs | 33 ++++++++++++------------ native/src/sepolicy/statement.cpp | 8 +++--- 18 files changed, 142 insertions(+), 68 deletions(-) diff --git a/native/src/base/cstr.rs b/native/src/base/cstr.rs index 6a10c4af7..ede577876 100644 --- a/native/src/base/cstr.rs +++ b/native/src/base/cstr.rs @@ -6,10 +6,12 @@ use std::path::Path; use std::str::{Utf8Chunks, Utf8Error}; use std::{fmt, mem, slice, str}; -use crate::slice_from_ptr_mut; +use cxx::{type_id, ExternType}; use libc::c_char; use thiserror::Error; +use crate::slice_from_ptr_mut; + // Utf8CStr types are UTF-8 validated and null terminated strings. // // Several Utf8CStr types: @@ -380,6 +382,22 @@ impl DerefMut for Utf8CStr { } } +// Notice that we only implement ExternType on Utf8CStr *reference* +unsafe impl ExternType for &Utf8CStr { + type Id = type_id!("rust::Utf8CStr"); + type Kind = cxx::kind::Trivial; +} + +macro_rules! const_assert_eq { + ($left:expr, $right:expr $(,)?) => { + const _: [(); $left] = [(); $right]; + }; +} + +// Assert ABI layout +const_assert_eq!(mem::size_of::<&Utf8CStr>(), mem::size_of::<[usize; 2]>()); +const_assert_eq!(mem::align_of::<&Utf8CStr>(), mem::align_of::<[usize; 2]>()); + // File system path extensions types #[repr(transparent)] diff --git a/native/src/base/cxx_extern.rs b/native/src/base/cxx_extern.rs index ec72e85a4..7c64a455f 100644 --- a/native/src/base/cxx_extern.rs +++ b/native/src/base/cxx_extern.rs @@ -10,7 +10,8 @@ use libc::mode_t; use crate::logging::CxxResultExt; pub(crate) use crate::xwrap::*; use crate::{ - clone_attr, fclone_attr, fd_path, map_fd, map_file, Directory, FsPath, Utf8CStr, Utf8CStrBufRef, + clone_attr, cstr, fclone_attr, fd_path, map_fd, map_file, slice_from_ptr, Directory, FsPath, + Utf8CStr, Utf8CStrBufRef, }; pub(crate) fn fd_path_for_cxx(fd: RawFd, buf: &mut [u8]) -> isize { @@ -57,12 +58,8 @@ unsafe extern "C" fn frm_rf(fd: OwnedFd) -> bool { inner(fd).is_ok() } -pub(crate) fn map_file_for_cxx(path: &[u8], rw: bool) -> &'static mut [u8] { - unsafe { - map_file(Utf8CStr::from_bytes_unchecked(path), rw) - .log_cxx() - .unwrap_or(&mut []) - } +pub(crate) fn map_file_for_cxx(path: &Utf8CStr, rw: bool) -> &'static mut [u8] { + map_file(path, rw).log_cxx().unwrap_or(&mut []) } pub(crate) fn map_fd_for_cxx(fd: RawFd, sz: usize, rw: bool) -> &'static mut [u8] { @@ -171,3 +168,18 @@ unsafe extern "C" fn fclone_attr_for_cxx(a: RawFd, b: RawFd) -> bool { .log_cxx_with_msg(|w| w.write_str("fclone_attr failed")) .is_ok() } + +#[export_name = "cxx$utf8str$new"] +unsafe extern "C" fn str_new(this: &mut &Utf8CStr, s: *const u8, len: usize) { + *this = Utf8CStr::from_bytes(slice_from_ptr(s, len)).unwrap_or(cstr!("")); +} + +#[export_name = "cxx$utf8str$ptr"] +unsafe extern "C" fn str_ptr(this: &&Utf8CStr) -> *const u8 { + this.as_ptr().cast() +} + +#[export_name = "cxx$utf8str$len"] +unsafe extern "C" fn str_len(this: &&Utf8CStr) -> usize { + this.len() +} diff --git a/native/src/base/files.cpp b/native/src/base/files.cpp index f1476c716..e91404470 100644 --- a/native/src/base/files.cpp +++ b/native/src/base/files.cpp @@ -189,7 +189,7 @@ sFILE make_file(FILE *fp) { } mmap_data::mmap_data(const char *name, bool rw) { - auto slice = rust::map_file(byte_view(name), rw); + auto slice = rust::map_file(name, rw); if (!slice.empty()) { _buf = slice.data(); _sz = slice.size(); diff --git a/native/src/base/lib.rs b/native/src/base/lib.rs index 286905ac8..04ea52638 100644 --- a/native/src/base/lib.rs +++ b/native/src/base/lib.rs @@ -34,12 +34,17 @@ pub mod ffi { unsafe extern "C++" { include!("misc.hpp"); + + #[namespace = "rust"] + #[cxx_name = "Utf8CStr"] + type Utf8CStrRef<'a> = &'a crate::cstr::Utf8CStr; + fn mut_u8_patch(buf: &mut [u8], from: &[u8], to: &[u8]) -> Vec; } extern "Rust" { #[cxx_name = "log_with_rs"] - fn log_from_cxx(level: LogLevelCxx, msg: &[u8]); + fn log_from_cxx(level: LogLevelCxx, msg: Utf8CStrRef); #[cxx_name = "set_log_level_state"] fn set_log_level_state_cxx(level: LogLevelCxx, enabled: bool); fn exit_on_error(b: bool); @@ -53,7 +58,7 @@ pub mod ffi { #[cxx_name = "fd_path"] fn fd_path_for_cxx(fd: i32, buf: &mut [u8]) -> isize; #[cxx_name = "map_file"] - fn map_file_for_cxx(path: &[u8], rw: bool) -> &'static mut [u8]; + fn map_file_for_cxx(path: Utf8CStrRef, rw: bool) -> &'static mut [u8]; #[cxx_name = "map_fd"] fn map_fd_for_cxx(fd: i32, sz: usize, rw: bool) -> &'static mut [u8]; } diff --git a/native/src/base/logging.cpp b/native/src/base/logging.cpp index 8157c1e7c..d0b62a89a 100644 --- a/native/src/base/logging.cpp +++ b/native/src/base/logging.cpp @@ -15,7 +15,7 @@ static int fmt_and_log_with_rs(LogLevel level, const char *fmt, va_list ap) { buf[0] = '\0'; // Fortify logs when a fatal error occurs. Do not run through fortify again int len = std::min(__call_bypassing_fortify(vsnprintf)(buf, sz, fmt, ap), sz - 1); - log_with_rs(level, byte_view(buf, len + 1)); + log_with_rs(level, rust::Utf8CStr(buf, len + 1)); return len; } diff --git a/native/src/base/logging.rs b/native/src/base/logging.rs index 34d83571e..e7b36606d 100644 --- a/native/src/base/logging.rs +++ b/native/src/base/logging.rs @@ -101,10 +101,8 @@ fn log_with_writer(level: LogLevel, f: F) { } } -pub fn log_from_cxx(level: LogLevelCxx, msg: &[u8]) { +pub fn log_from_cxx(level: LogLevelCxx, msg: &Utf8CStr) { if let Some(level) = LogLevel::from_i32(level.repr) { - // SAFETY: The null termination is handled on the C++ side - let msg = unsafe { Utf8CStr::from_bytes_unchecked(msg) }; log_with_writer(level, |write| write(level, msg)); } } diff --git a/native/src/base/misc.cpp b/native/src/base/misc.cpp index 313d136b6..7cf725345 100644 --- a/native/src/base/misc.cpp +++ b/native/src/base/misc.cpp @@ -274,3 +274,19 @@ int ssprintf(char *dest, size_t size, const char *fmt, ...) { size_t strscpy(char *dest, const char *src, size_t size) { return std::min(strlcpy(dest, src, size), size - 1); } + +extern "C" void cxx$utf8str$new(rust::Utf8CStr *self, const void *s, size_t len); +extern "C" const char *cxx$utf8str$ptr(const rust::Utf8CStr *self); +extern "C" size_t cxx$utf8str$len(const rust::Utf8CStr *self); + +rust::Utf8CStr::Utf8CStr(const char *s, size_t len) { + cxx$utf8str$new(this, s, len); +} + +const char *rust::Utf8CStr::data() const { + return cxx$utf8str$ptr(this); +} + +size_t rust::Utf8CStr::length() const { + return cxx$utf8str$len(this); +} diff --git a/native/src/base/misc.hpp b/native/src/base/misc.hpp index 1c084afa8..cc9253b8f 100644 --- a/native/src/base/misc.hpp +++ b/native/src/base/misc.hpp @@ -331,3 +331,30 @@ constexpr auto operator+(T e) noexcept -> std::enable_if_t::value, std::underlying_type_t> { return static_cast>(e); } + +namespace rust { + +struct Utf8CStr { + const char *data() const; + size_t length() const; + Utf8CStr(const char *s, size_t len); + + Utf8CStr() : Utf8CStr("", 1) {}; + Utf8CStr(const Utf8CStr &o) = default; + Utf8CStr(Utf8CStr &&o) = default; + Utf8CStr(const char *s) : Utf8CStr(s, strlen(s) + 1) {}; + Utf8CStr(std::string_view s) : Utf8CStr(s.data(), s.length() + 1) {}; + Utf8CStr(std::string s) : Utf8CStr(s.data(), s.length() + 1) {}; + const char *c_str() const { return this->data(); } + size_t size() const { return this->length(); } + bool empty() const { return this->length() == 0 ; } + operator std::string_view() { return {data(), length()}; } + +private: +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-private-field" + std::array repr; +#pragma clang diagnostic pop +}; + +} // namespace rust diff --git a/native/src/core/daemon.rs b/native/src/core/daemon.rs index b5710ba72..395d63e79 100644 --- a/native/src/core/daemon.rs +++ b/native/src/core/daemon.rs @@ -9,7 +9,7 @@ use base::{ Utf8CStrBufArr, Utf8CStrBufRef, WalkResult, }; -use crate::ffi::{CxxMagiskD, RequestCode}; +use crate::ffi::{get_magisk_tmp, CxxMagiskD, RequestCode}; use crate::logging::magisk_logging; use crate::{get_prop, MAIN_CONFIG}; @@ -97,10 +97,6 @@ impl MagiskD { } } -pub fn get_magisk_tmp() -> &'static Utf8CStr { - unsafe { Utf8CStr::from_ptr(super::ffi::get_magisk_tmp()).unwrap_unchecked() } -} - pub fn daemon_entry() { let mut qemu = get_prop(cstr!("ro.kernel.qemu"), false); if qemu.is_empty() { @@ -170,13 +166,9 @@ pub fn get_magiskd() -> &'static MagiskD { unsafe { MAGISKD.get().unwrap_unchecked() } } -pub fn find_apk_path(pkg: &[u8], data: &mut [u8]) -> usize { +pub fn find_apk_path(pkg: &Utf8CStr, data: &mut [u8]) -> usize { use WalkResult::*; - fn inner(pkg: &[u8], buf: &mut dyn Utf8CStrBuf) -> io::Result { - let pkg = match Utf8CStr::from_bytes(pkg) { - Ok(pkg) => pkg, - Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)), - }; + fn inner(pkg: &Utf8CStr, buf: &mut dyn Utf8CStrBuf) -> io::Result { Directory::open(cstr!("/data/app"))?.pre_order_walk(|e| { if !e.is_dir() { return Ok(Skip); diff --git a/native/src/core/include/daemon.hpp b/native/src/core/include/daemon.hpp index 7bbc0e54a..805d7107b 100644 --- a/native/src/core/include/daemon.hpp +++ b/native/src/core/include/daemon.hpp @@ -1,5 +1,7 @@ #pragma once +#include + namespace rust { struct MagiskD; } @@ -24,3 +26,4 @@ private: }; const char *get_magisk_tmp(); +static inline rust::Utf8CStr get_magisk_tmp_rs() { return get_magisk_tmp(); } diff --git a/native/src/core/lib.rs b/native/src/core/lib.rs index 7876fdbfa..43d0ceb06 100644 --- a/native/src/core/lib.rs +++ b/native/src/core/lib.rs @@ -58,9 +58,14 @@ pub mod ffi { } unsafe extern "C++" { + #[namespace = "rust"] + #[cxx_name = "Utf8CStr"] + type Utf8CStrRef<'a> = base::ffi::Utf8CStrRef<'a>; + include!("include/daemon.hpp"); - fn get_magisk_tmp() -> *const c_char; + #[cxx_name = "get_magisk_tmp_rs"] + fn get_magisk_tmp() -> Utf8CStrRef<'static>; #[cxx_name = "MagiskD"] type CxxMagiskD; @@ -76,12 +81,12 @@ pub mod ffi { fn zygisk_logging(); fn zygisk_close_logd(); fn zygisk_get_logd() -> i32; - fn find_apk_path(pkg: &[u8], data: &mut [u8]) -> usize; + fn find_apk_path(pkg: Utf8CStrRef, data: &mut [u8]) -> usize; fn read_certificate(fd: i32, version: i32) -> Vec; - unsafe fn persist_get_prop(name: *const c_char, prop_cb: Pin<&mut PropCb>); + unsafe fn persist_get_prop(name: Utf8CStrRef, prop_cb: Pin<&mut PropCb>); unsafe fn persist_get_props(prop_cb: Pin<&mut PropCb>); - unsafe fn persist_delete_prop(name: *const c_char) -> bool; - unsafe fn persist_set_prop(name: *const c_char, value: *const c_char) -> bool; + unsafe fn persist_delete_prop(name: Utf8CStrRef) -> bool; + unsafe fn persist_set_prop(name: Utf8CStrRef, value: Utf8CStrRef) -> bool; } #[namespace = "rust"] diff --git a/native/src/core/logging.rs b/native/src/core/logging.rs index 75991105d..f3afad14f 100644 --- a/native/src/core/logging.rs +++ b/native/src/core/logging.rs @@ -19,7 +19,8 @@ use base::libc::{ }; use base::*; -use crate::daemon::{get_magisk_tmp, MagiskD, MAGISKD}; +use crate::daemon::{MagiskD, MAGISKD}; +use crate::ffi::get_magisk_tmp; use crate::logging::LogFile::{Actual, Buffer}; use crate::{LOGFILE, LOG_PIPE}; diff --git a/native/src/core/package.cpp b/native/src/core/package.cpp index e26ba38ca..2db1c6f20 100644 --- a/native/src/core/package.cpp +++ b/native/src/core/package.cpp @@ -176,7 +176,7 @@ int get_manager(int user_id, string *pkg, bool install) { int app_id = to_app_id(st.st_uid); byte_array apk; - find_apk_path(byte_view(str[SU_MANAGER]), apk); + find_apk_path(str[SU_MANAGER], apk); int fd = xopen((const char *) apk.buf(), O_RDONLY | O_CLOEXEC); auto cert = read_certificate(fd, -1); close(fd); @@ -230,7 +230,7 @@ int get_manager(int user_id, string *pkg, bool install) { if (stat(app_path, &st) == 0) { #if ENFORCE_SIGNATURE byte_array apk; - find_apk_path(byte_view(JAVA_PACKAGE_NAME), apk); + find_apk_path(JAVA_PACKAGE_NAME, apk); int fd = xopen((const char *) apk.buf(), O_RDONLY | O_CLOEXEC); auto cert = read_certificate(fd, MAGISK_VER_CODE); close(fd); diff --git a/native/src/core/resetprop/persist.rs b/native/src/core/resetprop/persist.rs index 2aec56025..fdcea05d8 100644 --- a/native/src/core/resetprop/persist.rs +++ b/native/src/core/resetprop/persist.rs @@ -1,4 +1,3 @@ -use core::ffi::c_char; use std::io::Read; use std::{ fs::File, @@ -142,9 +141,8 @@ fn proto_write_props(props: &PersistentProperties) -> LoggedResult<()> { Ok(()) } -pub unsafe fn persist_get_prop(name: *const c_char, prop_cb: Pin<&mut PropCb>) { - fn inner(name: *const c_char, mut prop_cb: Pin<&mut PropCb>) -> LoggedResult<()> { - let name = unsafe { Utf8CStr::from_ptr(name)? }; +pub unsafe fn persist_get_prop(name: &Utf8CStr, prop_cb: Pin<&mut PropCb>) { + fn inner(name: &Utf8CStr, mut prop_cb: Pin<&mut PropCb>) -> LoggedResult<()> { if check_proto() { let mut props = proto_read_props()?; let prop = props.find(name)?; @@ -197,9 +195,8 @@ pub unsafe fn persist_get_props(prop_cb: Pin<&mut PropCb>) { inner(prop_cb).ok(); } -pub unsafe fn persist_delete_prop(name: *const c_char) -> bool { - fn inner(name: *const c_char) -> LoggedResult<()> { - let name = unsafe { Utf8CStr::from_ptr(name)? }; +pub unsafe fn persist_delete_prop(name: &Utf8CStr) -> bool { + fn inner(name: &Utf8CStr) -> LoggedResult<()> { if check_proto() { let mut props = proto_read_props()?; let idx = props.find_index(name).no_log()?; @@ -212,10 +209,8 @@ pub unsafe fn persist_delete_prop(name: *const c_char) -> bool { inner(name).is_ok() } -pub unsafe fn persist_set_prop(name: *const c_char, value: *const c_char) -> bool { - unsafe fn inner(name: *const c_char, value: *const c_char) -> LoggedResult<()> { - let name = Utf8CStr::from_ptr(name)?; - let value = Utf8CStr::from_ptr(value)?; +pub unsafe fn persist_set_prop(name: &Utf8CStr, value: &Utf8CStr) -> bool { + unsafe fn inner(name: &Utf8CStr, value: &Utf8CStr) -> LoggedResult<()> { if check_proto() { let mut props = proto_read_props()?; match props.find_index(name) { diff --git a/native/src/sepolicy/api.cpp b/native/src/sepolicy/api.cpp index 8dd5073dc..6db88cc9f 100644 --- a/native/src/sepolicy/api.cpp +++ b/native/src/sepolicy/api.cpp @@ -105,7 +105,7 @@ bool sepolicy::exists(const char *type) { } void sepolicy::load_rule_file(const char *file) { - rust::load_rule_file(*this, byte_view(file, false)); + rust::load_rule_file(*this, file); } void sepolicy::load_rules(const std::string &rules) { diff --git a/native/src/sepolicy/include/sepolicy.hpp b/native/src/sepolicy/include/sepolicy.hpp index 29e95b166..836c6ada4 100644 --- a/native/src/sepolicy/include/sepolicy.hpp +++ b/native/src/sepolicy/include/sepolicy.hpp @@ -3,6 +3,8 @@ #include #include +#include + // sepolicy paths #define PLAT_POLICY_DIR "/system/etc/selinux/" #define VEND_POLICY_DIR "/vendor/etc/selinux/" @@ -38,8 +40,7 @@ struct sepolicy { // External APIs bool to_file(c_str file); - void parse_statement(c_str stmt, int len); - void parse_statement(c_str stmt) { parse_statement(stmt, strlen(stmt)); } + void parse_statement(rust::Str stmt); void load_rules(const std::string &rules); void load_rule_file(c_str file); void print_rules(); diff --git a/native/src/sepolicy/lib.rs b/native/src/sepolicy/lib.rs index b10cf276d..524c47b68 100644 --- a/native/src/sepolicy/lib.rs +++ b/native/src/sepolicy/lib.rs @@ -1,33 +1,37 @@ use io::Cursor; -use std::fs::File; +use std::io; use std::io::{BufRead, BufReader}; use std::pin::Pin; -use std::{io, str}; pub use base; -use base::*; +use base::libc::{O_CLOEXEC, O_RDONLY}; +use base::{BufReadExt, FsPath, LoggedResult, Utf8CStr}; use crate::ffi::sepolicy; #[cxx::bridge] mod ffi { unsafe extern "C++" { + #[namespace = "rust"] + #[cxx_name = "Utf8CStr"] + type Utf8CStrRef<'a> = base::ffi::Utf8CStrRef<'a>; + include!("include/sepolicy.hpp"); type sepolicy; - unsafe fn parse_statement(self: Pin<&mut sepolicy>, stmt: *const c_char, len: i32); + fn parse_statement(self: Pin<&mut sepolicy>, stmt: &str); } #[namespace = "rust"] extern "Rust" { fn load_rules(sepol: Pin<&mut sepolicy>, rules: &[u8]); - fn load_rule_file(sepol: Pin<&mut sepolicy>, filename: &[u8]); + fn load_rule_file(sepol: Pin<&mut sepolicy>, filename: Utf8CStrRef); } } trait SepolicyExt { fn load_rules(self: Pin<&mut Self>, rules: &[u8]); - fn load_rule_file(self: Pin<&mut Self>, filename: &[u8]); + fn load_rule_file(self: Pin<&mut Self>, filename: &Utf8CStr); fn load_rules_from_reader(self: Pin<&mut Self>, reader: &mut T); } @@ -37,10 +41,10 @@ impl SepolicyExt for sepolicy { self.load_rules_from_reader(&mut cursor); } - fn load_rule_file(self: Pin<&mut sepolicy>, filename: &[u8]) { - fn inner(sepol: Pin<&mut sepolicy>, filename: &[u8]) -> LoggedResult<()> { - let filename = str::from_utf8(filename)?; - let mut reader = BufReader::new(File::open(filename)?); + fn load_rule_file(self: Pin<&mut sepolicy>, filename: &Utf8CStr) { + fn inner(sepol: Pin<&mut sepolicy>, filename: &Utf8CStr) -> LoggedResult<()> { + let file = FsPath::from(filename).open(O_RDONLY | O_CLOEXEC)?; + let mut reader = BufReader::new(file); sepol.load_rules_from_reader(&mut reader); Ok(()) } @@ -49,17 +53,14 @@ impl SepolicyExt for sepolicy { fn load_rules_from_reader(mut self: Pin<&mut sepolicy>, reader: &mut T) { reader.foreach_lines(|line| { - let bytes = line.trim().as_bytes(); - unsafe { - self.as_mut() - .parse_statement(bytes.as_ptr().cast(), bytes.len() as i32); - } + let line = line.trim(); + self.as_mut().parse_statement(line); true }); } } -pub fn load_rule_file(sepol: Pin<&mut sepolicy>, filename: &[u8]) { +pub fn load_rule_file(sepol: Pin<&mut sepolicy>, filename: &Utf8CStr) { sepol.load_rule_file(filename); } diff --git a/native/src/sepolicy/statement.cpp b/native/src/sepolicy/statement.cpp index d69a40b50..5e7b488c8 100644 --- a/native/src/sepolicy/statement.cpp +++ b/native/src/sepolicy/statement.cpp @@ -417,19 +417,19 @@ static bool parse_pattern_9(const Func &fn, const char *action, char *stmt) { else if (strcmp(name, action) == 0) { \ auto __fn = [&](auto && ...args){ return (fn)(args...); }; \ if (!parse_pattern_##type(__fn, name, remain)) \ - LOGW("Syntax error in '%.*s'\n\n%s\n", len, stmt, type_msg_##type); \ + LOGW("Syntax error in '%.*s'\n\n%s\n", (int) stmt.length(), stmt.data(), type_msg_##type); \ } #define add_action(act, type) add_action_func(#act, type, act) -void sepolicy::parse_statement(const char *stmt, int len) { +void sepolicy::parse_statement(rust::Str stmt) { // strtok modify strings, create a copy - string cpy(stmt, len); + string cpy(stmt.data(), stmt.length()); char *remain; char *action = strtok_r(cpy.data(), " ", &remain); if (remain == nullptr) { - LOGW("Syntax error in '%.*s'\n\n", len, stmt); + LOGW("Syntax error in '%.*s'\n\n", (int) stmt.length(), stmt.data()); return; }