use std::ffi::c_long;
use std::fmt;
use std::os::raw::c_double;
use std::os::raw::c_int;
use std::os::raw::c_longlong;
use std::ptr;
use std::ptr::NonNull;
use std::ptr::null_mut;
use std::slice;
use std::str;
use std::str::Utf8Error;
use jimtcl_sys::Jim_InvalidateStringRep;
use crate::JimResult;
use crate::error::JimError;
use crate::interp::Interp;
use crate::sys;
pub trait IntoJimObj {
fn to_jim<'jim>(self, interp: &'jim Interp) -> JimObject<'jim>;
}
pub struct JimObject<'jim> {
pub(crate) interp: &'jim Interp,
pub(crate) obj: *mut sys::Jim_Obj,
owned: bool,
}
impl<'jim> JimObject<'jim> {
pub fn empty(interp: &'jim Interp) -> JimObject<'jim> {
"".to_jim(interp)
}
pub fn len(&self) -> usize {
let size = unsafe { sys::Jim_Length(self.obj) };
assert!(size >= 0);
size as usize
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn wrap(interp: &'jim Interp, obj: *mut sys::Jim_Obj) -> JimObject<'jim> {
jim_incref(obj);
JimObject {
interp,
obj,
owned: true,
}
}
pub fn is_tainted(&self) -> bool {
if self.obj.is_null() {
false
} else {
unsafe { (*self.obj).taint != 0 }
}
}
pub fn taint(&self) {
#[cfg(feature = "taint")]
if !self.obj.is_null() {
unsafe { (*self.obj).taint = sys::JIM_TAINT_STD }
}
}
pub fn untaint(&self) {
#[cfg(feature = "taint")]
if !self.obj.is_null() {
unsafe { (*self.obj).taint = 0 }
}
}
pub fn require_untainted(&self, label: &str) -> JimResult<()> {
if self.is_tainted() {
Err(JimError::Error(format!("{} is tainted", label)))
} else {
Ok(())
}
}
pub fn get_raw(&self) -> *mut sys::Jim_Obj {
self.obj
}
pub(crate) fn wrap_unowned(interp: &'jim Interp, obj: *mut sys::Jim_Obj) -> JimObject<'jim> {
jim_incref(obj);
JimObject {
interp,
obj,
owned: false,
}
}
pub fn as_bytes(&self) -> &[u8] {
assert!(!self.is_null());
let mut len: c_int = 0;
unsafe {
let bytes = sys::Jim_GetString(self.obj, ptr::from_mut(&mut len));
slice::from_raw_parts(bytes.cast(), len as usize)
}
}
pub fn as_str(&self) -> Result<&str, Utf8Error> {
str::from_utf8(self.as_bytes())
}
pub(crate) fn as_ref_ptr(&self) -> *mut sys::Jim_Obj {
jim_incref(self.obj);
self.obj
}
pub fn as_index(&self) -> JimResult<i32> {
let mut index: c_int = 0;
let rc =
unsafe { sys::Jim_GetIndex(self.interp.interp, self.obj, ptr::from_mut(&mut index)) };
self.interp.require_ok(rc)?;
Ok(index as i32)
}
pub(crate) fn null(interp: &'jim Interp) -> JimObject<'jim> {
JimObject {
interp,
obj: null_mut(),
owned: false,
}
}
pub fn is_null(&self) -> bool {
self.obj.is_null()
}
pub(crate) fn non_null(self) -> Option<Self> {
if self.is_null() { None } else { Some(self) }
}
pub fn list_length(&self) -> usize {
let len = unsafe { sys::Jim_ListLength(self.interp.interp, self.obj) };
assert!(len >= 0);
len as usize
}
pub fn list_index(&self, index: usize) -> JimResult<JimObject<'jim>> {
let obj = unsafe { sys::Jim_ListGetIndex(self.interp.interp, self.obj, index as c_int) };
if obj.is_null() {
Err(JimError::InvalidIndex(index))
} else {
Ok(JimObject::wrap(self.interp, obj))
}
}
pub fn list_append<T: IntoJimObj>(&self, element: T) {
let obj = element.to_jim(self.interp);
self.list_append_obj(obj);
}
pub fn list_append_obj(&self, obj: JimObject<'jim>) {
unsafe {
sys::Jim_ListAppendElement(self.interp.interp, self.obj, obj.obj);
}
}
pub fn list_iter(&self) -> impl Iterator<Item = JimObject<'jim>> {
let len = self.list_length();
(0..len).map(|i| {
self.list_index(i)
.expect("invalid index, list modified during iteration?")
})
}
pub fn dict_size(&self) -> JimResult<usize> {
let size = unsafe { sys::Jim_DictSize(self.interp.interp, self.obj) };
if size >= 0 {
Ok(size as usize)
} else {
Err(JimError::InvalidDictionary)
}
}
pub fn dict_exists(&self, key: &str) -> JimResult<bool> {
Ok(self.dict_query(key)?.is_some())
}
pub fn dict_get(&self, key: &str) -> JimResult<Option<JimObject<'jim>>> {
Ok(self
.dict_query(key)?
.map(|p| JimObject::wrap(self.interp, p.as_ptr())))
}
fn dict_query(&self, key: &str) -> JimResult<Option<NonNull<sys::Jim_Obj>>> {
let key_obj = key.to_jim(self.interp);
let mut out = ptr::null_mut();
let rc = unsafe {
sys::Jim_DictKey(
self.interp.interp,
self.obj,
key_obj.obj,
ptr::from_mut(&mut out),
0,
)
};
if rc < 0 {
return Err(JimError::InvalidDictionary);
}
if rc as u32 == sys::JIM_ERR {
Ok(None)
} else {
self.interp.require_ok(rc)?;
Ok(NonNull::new(out))
}
}
pub fn dict_set<T: IntoJimObj>(&self, key: &str, val: T) -> JimResult<()> {
let ko = key.to_jim(self.interp);
let obj = val.to_jim(self.interp);
self.dict_set_obj(ko, obj)
}
pub fn dict_set_obj(&self, key: JimObject<'jim>, val: JimObject<'jim>) -> JimResult<()> {
let rc = unsafe { sys::Jim_DictAddElement(self.interp.interp, self.obj, key.obj, val.obj) };
self.interp.require_ok(rc)?;
Ok(())
}
pub fn dict_keys(&self) -> JimResult<Vec<String>> {
let len = self.dict_size()?;
let mut keys = Vec::with_capacity(len);
for i in 0..len {
keys.push(self.list_index(i * 2)?.to_string())
}
Ok(keys)
}
pub(crate) fn free_internal(&self) {
if self.obj.is_null() {
return;
}
unsafe {
let t = (*self.obj).typePtr;
if !t.is_null()
&& let Some(fp) = (*t).freeIntRepProc
{
fp(self.interp.interp, self.obj);
}
}
}
pub(crate) fn invalidate_string_rep(&self) {
if !self.is_null() {
unsafe {
Jim_InvalidateStringRep(self.obj);
}
}
}
}
impl<'jim> fmt::Display for JimObject<'jim> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.obj.is_null() {
f.write_str("<NULL>")
} else {
unsafe {
let mut len: c_int = 0;
let ptr = sys::Jim_GetString(self.obj, &mut len).cast();
let slice = slice::from_raw_parts(ptr, len as usize);
let repr = str::from_utf8(slice).expect("invalid string repr");
f.write_str(repr)
}
}
}
}
impl<'jim> fmt::Debug for JimObject<'jim> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
impl<'jim> Clone for JimObject<'jim> {
fn clone(&self) -> Self {
if self.owned {
jim_incref(self.obj);
}
JimObject {
interp: self.interp,
obj: self.obj,
owned: self.owned,
}
}
}
impl<'jim> Drop for JimObject<'jim> {
fn drop(&mut self) {
if self.owned {
jim_decref(self.interp, &mut self.obj);
}
}
}
pub(crate) fn jim_incref(obj: *mut sys::Jim_Obj) {
if !obj.is_null() {
unsafe { (*obj).refCount += 1 }
}
}
pub(crate) fn jim_decref(interp: &Interp, obj: &mut *mut sys::Jim_Obj) {
if !obj.is_null() {
unsafe {
(**obj).refCount -= 1;
if (**obj).refCount <= 0 {
interp.free_obj(*obj);
*obj = null_mut();
}
}
}
}
impl<'j2> IntoJimObj for JimObject<'j2> {
fn to_jim<'jim>(self, interp: &'jim Interp) -> JimObject<'jim> {
(&self).to_jim(interp)
}
}
impl<'j2> IntoJimObj for &JimObject<'j2> {
fn to_jim<'jim>(self, interp: &'jim Interp) -> JimObject<'jim> {
if self.interp.interp == interp.interp {
JimObject::wrap(interp, self.obj)
} else {
let str = self.as_bytes();
let obj = unsafe {
let obj = sys::Jim_NewObj(interp.interp);
let o = obj.as_mut().unwrap();
o.bytes = sys::Jim_StrDupLen(str.as_ptr().cast(), str.len() as c_int);
o.length = str.len() as c_int;
o.typePtr = ptr::null_mut();
obj
};
JimObject::wrap(interp, obj)
}
}
}
impl IntoJimObj for () {
fn to_jim<'jim>(self, interp: &'jim Interp) -> JimObject<'jim> {
JimObject::null(interp)
}
}
macro_rules! prim_int_jimcvt {
($type:ty) => {
impl IntoJimObj for $type {
fn to_jim<'jim>(self, interp: &'jim Interp) -> JimObject<'jim> {
unsafe {
JimObject::wrap(
interp,
sys::Jim_NewIntObj(interp.interp, self as c_longlong),
)
}
}
}
impl<'jim> TryFrom<&JimObject<'jim>> for $type {
type Error = JimError;
fn try_from(jim: &JimObject<'jim>) -> Result<$type, Self::Error> {
let mut int: c_long = 0;
let rc = unsafe {
sys::Jim_GetLong(jim.interp.interp, jim.obj, ptr::from_mut(&mut int))
};
jim.interp.require_ok(rc)?;
int.try_into()
.map_err(|_e| JimError::Error(format!("Jim integer out of bounds")))
}
}
};
}
prim_int_jimcvt!(u8);
prim_int_jimcvt!(i8);
prim_int_jimcvt!(u16);
prim_int_jimcvt!(i16);
prim_int_jimcvt!(u32);
prim_int_jimcvt!(i32);
prim_int_jimcvt!(u64);
prim_int_jimcvt!(i64);
prim_int_jimcvt!(usize);
prim_int_jimcvt!(isize);
macro_rules! prim_float_jimcvt {
($type:ty) => {
impl IntoJimObj for $type {
fn to_jim<'jim>(self, interp: &'jim Interp) -> JimObject<'jim> {
unsafe {
JimObject::wrap(
interp,
sys::Jim_NewDoubleObj(interp.interp, self as c_double),
)
}
}
}
impl<'jim> TryFrom<&JimObject<'jim>> for $type {
type Error = JimError;
fn try_from(jim: &JimObject<'jim>) -> Result<$type, Self::Error> {
let mut val: c_double = 0.0;
let rc = unsafe {
sys::Jim_GetDouble(jim.interp.interp, jim.obj, ptr::from_mut(&mut val))
};
jim.interp.require_ok(rc)?;
Ok(val as $type)
}
}
};
}
prim_float_jimcvt!(f32);
prim_float_jimcvt!(f64);
impl IntoJimObj for bool {
fn to_jim<'jim>(self, interp: &'jim Interp) -> JimObject<'jim> {
unsafe {
JimObject::wrap(
interp,
sys::Jim_NewIntObj(interp.interp, self as c_longlong),
)
}
}
}
impl<'jim> TryFrom<&JimObject<'jim>> for bool {
type Error = JimError;
fn try_from(jim: &JimObject<'jim>) -> Result<bool, Self::Error> {
let mut int: c_int = 0;
let rc =
unsafe { sys::Jim_GetBoolean(jim.interp.interp, jim.obj, ptr::from_mut(&mut int)) };
jim.interp.require_ok(rc)?;
Ok(int != 0)
}
}
impl IntoJimObj for &str {
fn to_jim<'jim>(self, interp: &'jim Interp) -> JimObject<'jim> {
unsafe {
JimObject::wrap(
interp,
sys::Jim_NewStringObj(
interp.interp,
self.as_ptr().cast(),
self.len().try_into().expect("invalid string length"),
),
)
}
}
}
impl IntoJimObj for &[u8] {
fn to_jim<'jim>(self, interp: &'jim Interp) -> JimObject<'jim> {
unsafe {
JimObject::wrap(
interp,
sys::Jim_NewStringObj(
interp.interp,
self.as_ptr().cast(),
self.len().try_into().expect("invalid string length"),
),
)
}
}
}
impl IntoJimObj for String {
fn to_jim<'jim>(self, interp: &'jim Interp) -> JimObject<'jim> {
self.as_str().to_jim(interp)
}
}
impl IntoJimObj for Vec<u8> {
fn to_jim<'jim>(self, interp: &'jim Interp) -> JimObject<'jim> {
self.as_slice().to_jim(interp)
}
}
impl<'jim> From<&JimObject<'jim>> for String {
fn from(value: &JimObject) -> Self {
value.to_string()
}
}
impl<T: IntoJimObj> IntoJimObj for Option<T> {
fn to_jim<'jim>(self, interp: &'jim Interp) -> JimObject<'jim> {
match self {
Some(v) => v.to_jim(interp),
None => JimObject::null(interp),
}
}
}
impl<'jim, T> TryFrom<&JimObject<'jim>> for Vec<T>
where
T: for<'a> TryFrom<&'a JimObject<'jim>>,
JimError: for<'a> From<<T as TryFrom<&'a JimObject<'jim>>>::Error>,
{
type Error = JimError;
fn try_from(value: &JimObject<'jim>) -> Result<Self, Self::Error> {
let len = value.list_length();
let mut vec = Vec::with_capacity(len);
for obj in value.list_iter() {
assert!(!obj.is_null());
let val = T::try_from(&obj)?;
vec.push(val);
}
Ok(vec)
}
}
#[test]
fn test_empty_string() -> JimResult<()> {
let interp = Interp::new()?;
let jim = JimObject::empty(&interp);
assert_eq!(&jim.to_string(), "");
assert_eq!(jim.list_length(), 0);
Ok(())
}
#[test]
fn test_empty_untainted() -> JimResult<()> {
let interp = Interp::new()?;
let jim = JimObject::empty(&interp);
assert!(!jim.is_tainted());
Ok(())
}
#[test]
#[cfg(feature = "taint")]
fn test_taint_object() -> JimResult<()> {
let interp = Interp::new()?;
let jim = interp.new_string("tainted love");
assert!(!jim.is_tainted());
jim.taint();
assert!(jim.is_tainted());
interp.set_variable("data", &jim)?;
let res = interp.eval("info tainted $data")?;
assert!(bool::try_from(&res)?);
Ok(())
}
#[test]
#[cfg(feature = "taint")]
fn test_tcl_taint_object() -> JimResult<()> {
let interp = Interp::new()?;
interp.eval("set object wombat")?;
interp.eval("taint object")?;
let jim = interp.get_variable("object");
assert!(jim.is_some());
let jim = jim.unwrap();
assert_eq!(jim.as_str()?, "wombat");
assert!(jim.is_tainted());
Ok(())
}
#[test]
#[cfg(feature = "taint")]
fn test_taint_disallowed() -> JimResult<()> {
let interp = Interp::new()?;
let jim = interp.new_string("hello");
jim.taint();
interp.set_variable("message", &jim)?;
assert!(jim.is_tainted());
let res = interp.eval("exec echo $message");
assert!(res.is_err());
Ok(())
}
#[test]
fn test_string_roundtrip() -> JimResult<()> {
let interp = Interp::new()?;
let msg = "HACKEM MUCHE";
let jim = msg.to_jim(&interp);
assert_eq!(&jim.to_string(), msg);
let out = jim.to_string();
assert_eq!(&out, msg);
Ok(())
}
#[test]
fn test_string_emoji() -> JimResult<()> {
let interp = Interp::new()?;
let msg = "hello 💃🏻";
let jim = msg.to_jim(&interp);
eprintln!("jim: {}", jim.as_str()?);
assert_eq!(jim.len(), msg.len());
assert_eq!(jim.as_str()?, msg);
Ok(())
}
#[test]
fn test_bytes_emoji() -> JimResult<()> {
let interp = Interp::new()?;
let msg = "hello 💃🏻";
let jim = msg.as_bytes().to_jim(&interp);
eprintln!("jim: {}", jim.as_str()?);
assert_eq!(jim.len(), msg.len());
assert_eq!(jim.as_str()?, msg);
Ok(())
}
#[test]
fn test_int_roundtrip() {
let interp = Interp::new().expect("failed to create Jim");
let jim = 42u32.to_jim(&interp);
let out = u32::try_from(&jim).expect("failed to convert");
assert_eq!(out, 42);
}
#[test]
fn test_int_bool() {
let interp = Interp::new().expect("failed to create Jim");
let jim = 42u32.to_jim(&interp);
let out = bool::try_from(&jim).expect("failed to convert");
assert!(out);
let jim = 0isize.to_jim(&interp);
let out = bool::try_from(&jim).expect("failed to convert");
assert!(!out);
}
#[test]
fn test_empty_list() -> JimResult<()> {
let interp = Interp::new()?;
let obj = "".to_jim(&interp);
let list = Vec::<String>::try_from(&obj)?;
assert_eq!(list.len(), 0);
Ok(())
}
#[test]
fn test_single_list() -> JimResult<()> {
let interp = Interp::new()?;
let obj = "bob".to_jim(&interp);
assert_eq!(obj.list_length(), 1);
let list = Vec::<String>::try_from(&obj)?;
assert_eq!(list.len(), 1);
assert_eq!(&list[0], "bob");
Ok(())
}
#[test]
fn test_double_list() -> JimResult<()> {
let interp = Interp::new()?;
let obj = "HACKEM MUCHE".to_jim(&interp);
assert_eq!(obj.list_length(), 2);
let list = Vec::<String>::try_from(&obj)?;
assert_eq!(list.len(), 2);
assert_eq!(&list[0], "HACKEM");
assert_eq!(&list[1], "MUCHE");
Ok(())
}
#[test]
fn test_list_append() -> JimResult<()> {
let interp = Interp::new()?;
let obj = "HACKEM".to_jim(&interp);
obj.list_append("MUCHE");
assert_eq!(obj.list_length(), 2);
assert_eq!(&obj.to_string(), "HACKEM MUCHE");
Ok(())
}
#[test]
fn test_list_append_extra() -> JimResult<()> {
let interp = Interp::new()?;
let obj = "scroll".to_jim(&interp);
obj.list_append("HACKEM MUCHE");
assert_eq!(obj.list_length(), 2);
assert_eq!(&obj.to_string(), "scroll {HACKEM MUCHE}");
Ok(())
}
#[test]
fn test_basic_dict() -> JimResult<()> {
let interp = Interp::new()?;
let obj = "k1 1 k2 5".to_jim(&interp);
assert_eq!(obj.list_length(), 4);
assert_eq!(obj.dict_size()?, 2);
assert!(obj.dict_exists("k1")?);
let v = i32::try_from(&obj.dict_get("k1")?.unwrap())?;
assert_eq!(v, 1);
assert!(obj.dict_exists("k2")?);
let v = i32::try_from(&obj.dict_get("k2")?.unwrap())?;
assert_eq!(v, 5);
assert!(!obj.dict_exists("nope")?);
let x = obj.dict_get("nope");
eprintln!("result: {:?}", x);
assert!(x.is_ok());
assert!(x?.is_none());
Ok(())
}
#[test]
fn test_dict_set_value() -> JimResult<()> {
let interp = Interp::new()?;
let obj = "foo bar".to_jim(&interp);
obj.dict_set("scroll", "HACKEM MUCHE")?;
assert_eq!(obj.dict_size()?, 2);
assert_eq!(&obj.to_string(), "foo bar scroll {HACKEM MUCHE}");
assert_eq!(
&obj.dict_get("scroll")?.unwrap().to_string(),
"HACKEM MUCHE"
);
assert!(obj.dict_exists("scroll")?);
Ok(())
}
#[test]
fn test_as_index() -> JimResult<()> {
let interp = Interp::new()?;
assert_eq!(interp.new_string("1").as_index()?, 1);
assert_eq!(interp.new_string("end-1").as_index()?, -2);
Ok(())
}