mirror of
https://github.com/FliegendeWurst/cursive.git
synced 2024-11-23 17:35:00 +00:00
Fix out-of-bounds access on TextView
Frequent updates to `TextContent` while the screen is being refreshed can trigger an out-of-bounds access: an update with a smaller string being performed between calls to `compute_rows()` and `draw()` on TextView will cause an out-of-bounds access when slicing the string at `Segment::resolve`. This change fixes the issue by caching the content value when performing size calculations and using this cached content when drawing the view. Some additional changes were also made to reuse `TextContent` invalidation code on `TextView` and wrap content data with `RefCell<Arc<..>>` to enable sharing the inner string between `content_value` and `content_cache`.
This commit is contained in:
parent
c0004c54c0
commit
dc10fd8c44
@ -1,4 +1,6 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use std::ptr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::{Mutex, MutexGuard};
|
use std::sync::{Mutex, MutexGuard};
|
||||||
|
|
||||||
@ -12,6 +14,9 @@ use crate::utils::markup::StyledString;
|
|||||||
use crate::view::{SizeCache, View};
|
use crate::view::{SizeCache, View};
|
||||||
use crate::{Printer, Vec2, With, XY};
|
use crate::{Printer, Vec2, With, XY};
|
||||||
|
|
||||||
|
// Content type used internally for caching and storage
|
||||||
|
type InnerContentType = RefCell<Arc<StyledString>>;
|
||||||
|
|
||||||
/// Provides access to the content of a [`TextView`].
|
/// Provides access to the content of a [`TextView`].
|
||||||
///
|
///
|
||||||
/// Cloning this object will still point to the same content.
|
/// Cloning this object will still point to the same content.
|
||||||
@ -42,11 +47,12 @@ impl TextContent {
|
|||||||
where
|
where
|
||||||
S: Into<StyledString>,
|
S: Into<StyledString>,
|
||||||
{
|
{
|
||||||
let content = content.into();
|
let content = Arc::new(content.into());
|
||||||
|
|
||||||
TextContent {
|
TextContent {
|
||||||
content: Arc::new(Mutex::new(TextContentInner {
|
content: Arc::new(Mutex::new(TextContentInner {
|
||||||
content,
|
content_value: RefCell::new(content),
|
||||||
|
content_cache: RefCell::new(Arc::new(StyledString::default())),
|
||||||
size_cache: None,
|
size_cache: None,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
@ -60,18 +66,22 @@ impl TextContent {
|
|||||||
/// [`StyledString`]: ../utils/markup/type.StyledString.html
|
/// [`StyledString`]: ../utils/markup/type.StyledString.html
|
||||||
///
|
///
|
||||||
/// This keeps the content locked. Do not store this!
|
/// This keeps the content locked. Do not store this!
|
||||||
|
|
||||||
pub struct TextContentRef {
|
pub struct TextContentRef {
|
||||||
handle: OwningHandle<
|
_handle: OwningHandle<
|
||||||
ArcRef<Mutex<TextContentInner>>,
|
ArcRef<Mutex<TextContentInner>>,
|
||||||
MutexGuard<'static, TextContentInner>,
|
MutexGuard<'static, TextContentInner>,
|
||||||
>,
|
>,
|
||||||
|
// We also need to keep a copy of Arc so `deref` can return
|
||||||
|
// a reference to the `StyledString`
|
||||||
|
data: Arc<StyledString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for TextContentRef {
|
impl Deref for TextContentRef {
|
||||||
type Target = StyledString;
|
type Target = StyledString;
|
||||||
|
|
||||||
fn deref(&self) -> &StyledString {
|
fn deref(&self) -> &StyledString {
|
||||||
&self.handle.content
|
self.data.as_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +91,9 @@ impl TextContent {
|
|||||||
where
|
where
|
||||||
S: Into<StyledString>,
|
S: Into<StyledString>,
|
||||||
{
|
{
|
||||||
self.with_content(|c| *c = content.into());
|
self.with_content(|c| {
|
||||||
|
c.content_value.replace(Arc::new(content.into()))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Append `content` to the end of a `TextView`.
|
/// Append `content` to the end of a `TextView`.
|
||||||
@ -89,7 +101,17 @@ impl TextContent {
|
|||||||
where
|
where
|
||||||
S: Into<StyledString>,
|
S: Into<StyledString>,
|
||||||
{
|
{
|
||||||
self.with_content(|c| c.append(content))
|
self.with_content(|c| {
|
||||||
|
// This will only clone content if content_cached and content_value
|
||||||
|
// are sharing the same underlying Rc.
|
||||||
|
if c.is_content_shared() {
|
||||||
|
c.content_value
|
||||||
|
.replace(Arc::new(c.get_value().as_ref().clone()));
|
||||||
|
};
|
||||||
|
Arc::get_mut(&mut c.content_value.borrow_mut())
|
||||||
|
.expect("should not have a shared content here")
|
||||||
|
.append(content)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a reference to the content.
|
/// Returns a reference to the content.
|
||||||
@ -102,11 +124,11 @@ impl TextContent {
|
|||||||
|
|
||||||
fn with_content<F, O>(&mut self, f: F) -> O
|
fn with_content<F, O>(&mut self, f: F) -> O
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut StyledString) -> O,
|
F: FnOnce(&mut TextContentInner) -> O,
|
||||||
{
|
{
|
||||||
let mut lock = self.content.lock().unwrap();
|
let mut lock = self.content.lock().unwrap();
|
||||||
|
|
||||||
let out = f(&mut lock.content);
|
let out = f(&mut lock);
|
||||||
|
|
||||||
lock.size_cache = None;
|
lock.size_cache = None;
|
||||||
|
|
||||||
@ -121,7 +143,8 @@ impl TextContent {
|
|||||||
/// Can be shared (through a `Arc<Mutex>`).
|
/// Can be shared (through a `Arc<Mutex>`).
|
||||||
struct TextContentInner {
|
struct TextContentInner {
|
||||||
// content: String,
|
// content: String,
|
||||||
content: StyledString,
|
content_value: InnerContentType,
|
||||||
|
content_cache: InnerContentType,
|
||||||
|
|
||||||
// We keep the cache here so it can be busted when we change the content.
|
// We keep the cache here so it can be busted when we change the content.
|
||||||
size_cache: Option<XY<SizeCache>>,
|
size_cache: Option<XY<SizeCache>>,
|
||||||
@ -133,11 +156,13 @@ impl TextContentInner {
|
|||||||
let arc_ref: ArcRef<Mutex<TextContentInner>> =
|
let arc_ref: ArcRef<Mutex<TextContentInner>> =
|
||||||
ArcRef::new(Arc::clone(content));
|
ArcRef::new(Arc::clone(content));
|
||||||
|
|
||||||
TextContentRef {
|
let _handle = OwningHandle::new_with_fn(arc_ref, |mutex| unsafe {
|
||||||
handle: OwningHandle::new_with_fn(arc_ref, |mutex| unsafe {
|
(*mutex).lock().unwrap()
|
||||||
(*mutex).lock().unwrap()
|
});
|
||||||
}),
|
|
||||||
}
|
let data = _handle.get_value();
|
||||||
|
|
||||||
|
TextContentRef { _handle, data }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_cache_valid(&self, size: Vec2) -> bool {
|
fn is_cache_valid(&self, size: Vec2) -> bool {
|
||||||
@ -146,6 +171,18 @@ impl TextContentInner {
|
|||||||
Some(ref last) => last.x.accept(size.x) && last.y.accept(size.y),
|
Some(ref last) => last.x.accept(size.x) && last.y.accept(size.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_value(&self) -> Arc<StyledString> {
|
||||||
|
Arc::clone(&*self.content_value.borrow())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cache(&self) -> Arc<StyledString> {
|
||||||
|
Arc::clone(&*self.content_cache.borrow())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_content_shared(&self) -> bool {
|
||||||
|
ptr::eq(self.get_value().as_ref(), self.get_cache().as_ref())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A simple view showing a fixed text.
|
/// A simple view showing a fixed text.
|
||||||
@ -161,7 +198,7 @@ impl TextContentInner {
|
|||||||
/// ```
|
/// ```
|
||||||
pub struct TextView {
|
pub struct TextView {
|
||||||
// content: String,
|
// content: String,
|
||||||
content: Arc<Mutex<TextContentInner>>,
|
content: TextContent,
|
||||||
rows: Vec<Row>,
|
rows: Vec<Row>,
|
||||||
|
|
||||||
align: Align,
|
align: Align,
|
||||||
@ -202,7 +239,7 @@ impl TextView {
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn new_with_content(content: TextContent) -> Self {
|
pub fn new_with_content(content: TextContent) -> Self {
|
||||||
TextView {
|
TextView {
|
||||||
content: content.content,
|
content: content,
|
||||||
effect: Effect::Simple,
|
effect: Effect::Simple,
|
||||||
rows: Vec::new(),
|
rows: Vec::new(),
|
||||||
wrap: true,
|
wrap: true,
|
||||||
@ -285,8 +322,7 @@ impl TextView {
|
|||||||
where
|
where
|
||||||
S: Into<StyledString>,
|
S: Into<StyledString>,
|
||||||
{
|
{
|
||||||
self.content.lock().unwrap().content = content.into();
|
self.content.set_content(content);
|
||||||
self.invalidate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Append `content` to the end of a `TextView`.
|
/// Append `content` to the end of a `TextView`.
|
||||||
@ -294,22 +330,20 @@ impl TextView {
|
|||||||
where
|
where
|
||||||
S: Into<StyledString>,
|
S: Into<StyledString>,
|
||||||
{
|
{
|
||||||
self.content.lock().unwrap().content.append(content.into());
|
self.content.append(content);
|
||||||
self.invalidate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current text in this view.
|
/// Returns the current text in this view.
|
||||||
pub fn get_content(&self) -> TextContentRef {
|
pub fn get_content(&self) -> TextContentRef {
|
||||||
TextContentInner::get_content(&self.content)
|
TextContentInner::get_content(&self.content.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a shared reference to the content, allowing content mutation.
|
/// Returns a shared reference to the content, allowing content mutation.
|
||||||
pub fn get_shared_content(&mut self) -> TextContent {
|
pub fn get_shared_content(&mut self) -> TextContent {
|
||||||
// We take &mut here without really needing it,
|
// We take &mut here without really needing it,
|
||||||
// because it sort of "makes sense".
|
// because it sort of "makes sense".
|
||||||
|
|
||||||
TextContent {
|
TextContent {
|
||||||
content: Arc::clone(&self.content),
|
content: Arc::clone(&self.content.content),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,7 +352,7 @@ impl TextView {
|
|||||||
fn compute_rows(&mut self, size: Vec2) {
|
fn compute_rows(&mut self, size: Vec2) {
|
||||||
let size = if self.wrap { size } else { Vec2::max_value() };
|
let size = if self.wrap { size } else { Vec2::max_value() };
|
||||||
|
|
||||||
let mut content = self.content.lock().unwrap();
|
let mut content = self.content.content.lock().unwrap();
|
||||||
if content.is_cache_valid(size) {
|
if content.is_cache_valid(size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -326,23 +360,21 @@ impl TextView {
|
|||||||
// Completely bust the cache
|
// Completely bust the cache
|
||||||
// Just in case we fail, we don't want to leave a bad cache.
|
// Just in case we fail, we don't want to leave a bad cache.
|
||||||
content.size_cache = None;
|
content.size_cache = None;
|
||||||
|
content
|
||||||
|
.content_cache
|
||||||
|
.replace(content.content_value.borrow().clone());
|
||||||
|
|
||||||
if size.x == 0 {
|
if size.x == 0 {
|
||||||
// Nothing we can do at this point.
|
// Nothing we can do at this point.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.rows = LinesIterator::new(&content.content, size.x).collect();
|
self.rows =
|
||||||
|
LinesIterator::new(content.get_cache().as_ref(), size.x).collect();
|
||||||
|
|
||||||
// Desired width
|
// Desired width
|
||||||
self.width = self.rows.iter().map(|row| row.width).max();
|
self.width = self.rows.iter().map(|row| row.width).max();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidates the cache, so next call will recompute everything.
|
|
||||||
fn invalidate(&mut self) {
|
|
||||||
let mut content = self.content.lock().unwrap();
|
|
||||||
content.size_cache = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl View for TextView {
|
impl View for TextView {
|
||||||
@ -352,14 +384,14 @@ impl View for TextView {
|
|||||||
let offset = self.align.v.get_offset(h, printer.size.y);
|
let offset = self.align.v.get_offset(h, printer.size.y);
|
||||||
let printer = &printer.offset((0, offset));
|
let printer = &printer.offset((0, offset));
|
||||||
|
|
||||||
let content = self.content.lock().unwrap();
|
let content = self.content.content.lock().unwrap();
|
||||||
|
|
||||||
printer.with_effect(self.effect, |printer| {
|
printer.with_effect(self.effect, |printer| {
|
||||||
for (y, row) in self.rows.iter().enumerate() {
|
for (y, row) in self.rows.iter().enumerate() {
|
||||||
let l = row.width;
|
let l = row.width;
|
||||||
let mut x = self.align.h.get_offset(l, printer.size.x);
|
let mut x = self.align.h.get_offset(l, printer.size.x);
|
||||||
|
|
||||||
for span in row.resolve(&content.content) {
|
for span in row.resolve(content.get_cache().as_ref()) {
|
||||||
printer.with_style(*span.attr, |printer| {
|
printer.with_style(*span.attr, |printer| {
|
||||||
printer.print((x, y), span.content);
|
printer.print((x, y), span.content);
|
||||||
x += span.content.width();
|
x += span.content.width();
|
||||||
@ -370,7 +402,7 @@ impl View for TextView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn needs_relayout(&self) -> bool {
|
fn needs_relayout(&self) -> bool {
|
||||||
let content = self.content.lock().unwrap();
|
let content = self.content.content.lock().unwrap();
|
||||||
content.size_cache.is_none()
|
content.size_cache.is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -389,7 +421,7 @@ impl View for TextView {
|
|||||||
let my_size = Vec2::new(self.width.unwrap_or(0), self.rows.len());
|
let my_size = Vec2::new(self.width.unwrap_or(0), self.rows.len());
|
||||||
|
|
||||||
// Build a fresh cache.
|
// Build a fresh cache.
|
||||||
let mut content = self.content.lock().unwrap();
|
let mut content = self.content.content.lock().unwrap();
|
||||||
content.size_cache = Some(SizeCache::build(my_size, size));
|
content.size_cache = Some(SizeCache::build(my_size, size));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user