Defensive copy in SpannableBuilder
This commit is contained in:
		
							parent
							
								
									e37d6e1729
								
							
						
					
					
						commit
						1f403abeb0
					
				@ -2,12 +2,8 @@ package ru.noties.markwon;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import android.app.Activity;
 | 
					import android.app.Activity;
 | 
				
			||||||
import android.content.Intent;
 | 
					import android.content.Intent;
 | 
				
			||||||
import android.graphics.Canvas;
 | 
					 | 
				
			||||||
import android.graphics.Paint;
 | 
					 | 
				
			||||||
import android.net.Uri;
 | 
					import android.net.Uri;
 | 
				
			||||||
import android.os.Bundle;
 | 
					import android.os.Bundle;
 | 
				
			||||||
import android.text.Layout;
 | 
					 | 
				
			||||||
import android.text.style.LeadingMarginSpan;
 | 
					 | 
				
			||||||
import android.view.View;
 | 
					import android.view.View;
 | 
				
			||||||
import android.widget.TextView;
 | 
					import android.widget.TextView;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -62,56 +58,6 @@ public class MainActivity extends Activity {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        appBarRenderer.render(appBarState());
 | 
					        appBarRenderer.render(appBarState());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (false) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            final class Whatever {
 | 
					 | 
				
			||||||
                CharSequence text() {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    final SpannableBuilder builder = new SpannableBuilder();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    builder.append("First line\n\n");
 | 
					 | 
				
			||||||
                    builder.append(Markwon.markdown(MainActivity.this, "* first\n* second\n* * third\n* * * forth\n\n"));
 | 
					 | 
				
			||||||
                    builder.setSpan(new LeadingMarginSpan() {
 | 
					 | 
				
			||||||
                        @Override
 | 
					 | 
				
			||||||
                        public int getLeadingMargin(boolean first) {
 | 
					 | 
				
			||||||
                            return 100;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        @Override
 | 
					 | 
				
			||||||
                        public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }, 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    builder.append("Last line\n\n");
 | 
					 | 
				
			||||||
                    return builder.text();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            final Whatever whatever = new Whatever();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            final SpannableBuilder builder = new SpannableBuilder();
 | 
					 | 
				
			||||||
            builder.append(whatever.text());
 | 
					 | 
				
			||||||
            builder.append(whatever.text());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            builder.setSpan(new LeadingMarginSpan() {
 | 
					 | 
				
			||||||
                @Override
 | 
					 | 
				
			||||||
                public int getLeadingMargin(boolean first) {
 | 
					 | 
				
			||||||
                    return 50;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                @Override
 | 
					 | 
				
			||||||
                public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }, 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            builder.append(whatever.text());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            textView.setText(builder.text());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() {
 | 
					        markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() {
 | 
				
			||||||
            @Override
 | 
					            @Override
 | 
				
			||||||
            public void apply(final String text) {
 | 
					            public void apply(final String text) {
 | 
				
			||||||
 | 
				
			|||||||
@ -14,6 +14,8 @@ import java.util.Iterator;
 | 
				
			|||||||
 * is using an array to store all the information about spans. So, a span that is added first
 | 
					 * is using an array to store all the information about spans. So, a span that is added first
 | 
				
			||||||
 * will be drawn first, which leads to subtle bugs (spans receive wrong `x` values when
 | 
					 * will be drawn first, which leads to subtle bugs (spans receive wrong `x` values when
 | 
				
			||||||
 * requested to draw itself)
 | 
					 * requested to draw itself)
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @since 1.0.1
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@SuppressWarnings({"WeakerAccess", "unused"})
 | 
					@SuppressWarnings({"WeakerAccess", "unused"})
 | 
				
			||||||
public class SpannableBuilder {
 | 
					public class SpannableBuilder {
 | 
				
			||||||
@ -23,6 +25,8 @@ public class SpannableBuilder {
 | 
				
			|||||||
    // we will be using SpannableStringBuilder anyway as a backing store
 | 
					    // we will be using SpannableStringBuilder anyway as a backing store
 | 
				
			||||||
    // as it has tight connection with system (implements some hidden methods, etc)
 | 
					    // as it has tight connection with system (implements some hidden methods, etc)
 | 
				
			||||||
    private final SpannableStringBuilder builder;
 | 
					    private final SpannableStringBuilder builder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // actually we might be just using ArrayList
 | 
				
			||||||
    private final Deque<Span> spans = new ArrayDeque<>(8);
 | 
					    private final Deque<Span> spans = new ArrayDeque<>(8);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public SpannableBuilder() {
 | 
					    public SpannableBuilder() {
 | 
				
			||||||
@ -31,7 +35,7 @@ public class SpannableBuilder {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public SpannableBuilder(@NonNull CharSequence cs) {
 | 
					    public SpannableBuilder(@NonNull CharSequence cs) {
 | 
				
			||||||
        this.builder = new SpannableStringBuilderImpl(cs.toString());
 | 
					        this.builder = new SpannableStringBuilderImpl(cs.toString());
 | 
				
			||||||
        copySpans(cs);
 | 
					        copySpans(0, cs);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -55,7 +59,7 @@ public class SpannableBuilder {
 | 
				
			|||||||
    @NonNull
 | 
					    @NonNull
 | 
				
			||||||
    public SpannableBuilder append(@NonNull CharSequence cs) {
 | 
					    public SpannableBuilder append(@NonNull CharSequence cs) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        copySpans(cs);
 | 
					        copySpans(length(), cs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        builder.append(cs.toString());
 | 
					        builder.append(cs.toString());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -109,7 +113,8 @@ public class SpannableBuilder {
 | 
				
			|||||||
    @NonNull
 | 
					    @NonNull
 | 
				
			||||||
    public CharSequence remove(int start, int end) {
 | 
					    public CharSequence remove(int start, int end) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // okay: here is what we will try to do:
 | 
					        // this method is intended to be used only by markdown visitor
 | 
				
			||||||
 | 
					        // it's a workaround to allow tables
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final SpannableStringBuilderImpl impl = new SpannableStringBuilderImpl(builder.subSequence(start, end));
 | 
					        final SpannableStringBuilderImpl impl = new SpannableStringBuilderImpl(builder.subSequence(start, end));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -165,24 +170,26 @@ public class SpannableBuilder {
 | 
				
			|||||||
        return builder.toString();
 | 
					        return builder.toString();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Unfortunately I cannot see any way to NOT expose this internal value, which opens a gate
 | 
					 | 
				
			||||||
    // to external modification (first of all InputFilters, that potentially break span indexes
 | 
					 | 
				
			||||||
    // as we keep track of them independently). Must warn user to NOT apply inputFilters
 | 
					 | 
				
			||||||
    @NonNull
 | 
					    @NonNull
 | 
				
			||||||
    public CharSequence text() {
 | 
					    public CharSequence text() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // if called once, it will apply spans, which will modify our state
 | 
					        // okay, in order to not allow external modification and keep our spans order
 | 
				
			||||||
 | 
					        // we should not return our builder
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					        // plus, if this method was called -> all spans would be applied, which potentially
 | 
				
			||||||
 | 
					        // breaks the order that we intend to use
 | 
				
			||||||
 | 
					        // so, we will defensively copy builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        applySpans();
 | 
					        final SpannableStringBuilderImpl impl = new SpannableStringBuilderImpl(builder.toString());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // we could return here for example new SpannableStringBuilder(builder)
 | 
					        for (Span span : spans) {
 | 
				
			||||||
        // but, if returned value will be used in other SpannableBuilder,
 | 
					            impl.setSpan(span.what, span.start, span.end, span.flags);
 | 
				
			||||||
        // we won't be able to detect in what order to store the spans
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return builder;
 | 
					        return impl;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private void copySpans(@Nullable CharSequence cs) {
 | 
					    private void copySpans(final int index, @Nullable CharSequence cs) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // we must identify already reversed Spanned...
 | 
					        // we must identify already reversed Spanned...
 | 
				
			||||||
        // and (!) iterate backwards when adding (to preserve order)
 | 
					        // and (!) iterate backwards when adding (to preserve order)
 | 
				
			||||||
@ -191,7 +198,6 @@ public class SpannableBuilder {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            final Spanned spanned = (Spanned) cs;
 | 
					            final Spanned spanned = (Spanned) cs;
 | 
				
			||||||
            final boolean reverse = spanned instanceof SpannedReversed;
 | 
					            final boolean reverse = spanned instanceof SpannedReversed;
 | 
				
			||||||
            final int index = length();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
 | 
					            final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -209,17 +215,6 @@ public class SpannableBuilder {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private void applySpans() {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // will apply appended spans in reverse order
 | 
					 | 
				
			||||||
        // clear the stack (that keeps track of them)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Span span;
 | 
					 | 
				
			||||||
        while ((span = spans.poll()) != null) {
 | 
					 | 
				
			||||||
            builder.setSpan(span.what, span.start, span.end, span.flags);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private static class Span {
 | 
					    private static class Span {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final Object what;
 | 
					        final Object what;
 | 
				
			||||||
 | 
				
			|||||||
@ -2,12 +2,11 @@ package ru.noties.markwon;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import android.text.SpannableStringBuilder;
 | 
					import android.text.SpannableStringBuilder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @since 1.0.1
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
class SpannableStringBuilderImpl extends SpannableStringBuilder implements SpannedReversed {
 | 
					class SpannableStringBuilderImpl extends SpannableStringBuilder implements SpannedReversed {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    SpannableStringBuilderImpl() {
 | 
					 | 
				
			||||||
        super();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    SpannableStringBuilderImpl(CharSequence text) {
 | 
					    SpannableStringBuilderImpl(CharSequence text) {
 | 
				
			||||||
        super(text);
 | 
					        super(text);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user