Skip to content

Commit 2909994

Browse files
antonfirsovJimBobSquarePants
authored andcommitted
Throw ObjectDisposedException when trying to operate on a disposed image (#968)
* disable multitargeting + TreatWarningsAsErrors to for fast development * Check if image is disposed in significant Image and Image<T> methods * Mutate / Clone: ensure image is not disposed * Revert "disable multitargeting + TreatWarningsAsErrors to for fast development" This reverts commit 9ad74f7.
1 parent 6f11341 commit 2909994

File tree

7 files changed

+156
-10
lines changed

7 files changed

+156
-10
lines changed

src/ImageSharp/Image.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,22 @@ internal Image(
8080
/// </summary>
8181
Configuration IConfigurable.Configuration => this.Configuration;
8282

83+
/// <summary>
84+
/// Gets a value indicating whether the image instance is disposed.
85+
/// </summary>
86+
public bool IsDisposed { get; private set; }
87+
8388
/// <inheritdoc />
84-
public abstract void Dispose();
89+
public void Dispose()
90+
{
91+
if (this.IsDisposed)
92+
{
93+
return;
94+
}
95+
96+
this.IsDisposed = true;
97+
this.DisposeImpl();
98+
}
8599

86100
/// <summary>
87101
/// Saves the image to the given stream using the given image encoder.
@@ -93,6 +107,7 @@ public void Save(Stream stream, IImageEncoder encoder)
93107
{
94108
Guard.NotNull(stream, nameof(stream));
95109
Guard.NotNull(encoder, nameof(encoder));
110+
this.EnsureNotDisposed();
96111

97112
EncodeVisitor visitor = new EncodeVisitor(encoder, stream);
98113
this.AcceptVisitor(visitor);
@@ -128,6 +143,11 @@ public abstract Image<TPixel2> CloneAs<TPixel2>(Configuration configuration)
128143
/// <param name="size">The <see cref="Size"/>.</param>
129144
protected void UpdateSize(Size size) => this.size = size;
130145

146+
/// <summary>
147+
/// Implements the Dispose logic.
148+
/// </summary>
149+
protected abstract void DisposeImpl();
150+
131151
private class EncodeVisitor : IImageVisitor
132152
{
133153
private readonly IImageEncoder encoder;

src/ImageSharp/ImageExtensions.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,5 +119,16 @@ public static string ToBase64String<TPixel>(this Image<TPixel> source, IImageFor
119119
return $"data:{format.DefaultMimeType};base64,{Convert.ToBase64String(stream.ToArray())}";
120120
}
121121
}
122+
123+
/// <summary>
124+
/// Throws <see cref="ObjectDisposedException"/> if the image is disposed.
125+
/// </summary>
126+
internal static void EnsureNotDisposed(this Image image)
127+
{
128+
if (image.IsDisposed)
129+
{
130+
throw new ObjectDisposedException(nameof(image), "Trying to execute an operation on a disposed image.");
131+
}
132+
}
122133
}
123-
}
134+
}

src/ImageSharp/Image{TPixel}.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ internal Image(Configuration configuration, ImageMetadata metadata, IEnumerable<
162162
/// <returns>Returns a new <see cref="Image{TPixel}"/> with all the same pixel data as the original.</returns>
163163
public Image<TPixel> Clone(Configuration configuration)
164164
{
165+
this.EnsureNotDisposed();
166+
165167
IEnumerable<ImageFrame<TPixel>> clonedFrames =
166168
this.Frames.Select<ImageFrame<TPixel>, ImageFrame<TPixel>>(x => x.Clone(configuration));
167169
return new Image<TPixel>(configuration, this.Metadata.DeepClone(), clonedFrames);
@@ -175,17 +177,21 @@ public Image<TPixel> Clone(Configuration configuration)
175177
/// <returns>The <see cref="Image{TPixel2}"/>.</returns>
176178
public override Image<TPixel2> CloneAs<TPixel2>(Configuration configuration)
177179
{
180+
this.EnsureNotDisposed();
181+
178182
IEnumerable<ImageFrame<TPixel2>> clonedFrames =
179183
this.Frames.Select<ImageFrame<TPixel>, ImageFrame<TPixel2>>(x => x.CloneAs<TPixel2>(configuration));
180184
return new Image<TPixel2>(configuration, this.Metadata.DeepClone(), clonedFrames);
181185
}
182186

183187
/// <inheritdoc/>
184-
public override void Dispose() => this.Frames.Dispose();
188+
protected override void DisposeImpl() => this.Frames.Dispose();
185189

186190
/// <inheritdoc />
187191
internal override void AcceptVisitor(IImageVisitor visitor)
188192
{
193+
this.EnsureNotDisposed();
194+
189195
visitor.Visit(this);
190196
}
191197

src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public static class ProcessingExtensions
2121
/// <param name="operation">The operation to perform on the source.</param>
2222
public static void Mutate(this Image source, Action<IImageProcessingContext> operation)
2323
{
24+
Guard.NotNull(source, nameof(source));
25+
Guard.NotNull(operation, nameof(operation));
26+
source.EnsureNotDisposed();
27+
2428
ProcessingVisitor visitor = new ProcessingVisitor(operation, true);
2529
source.AcceptVisitor(visitor);
2630
}
@@ -34,8 +38,9 @@ public static void Mutate(this Image source, Action<IImageProcessingContext> ope
3438
public static void Mutate<TPixel>(this Image<TPixel> source, Action<IImageProcessingContext> operation)
3539
where TPixel : struct, IPixel<TPixel>
3640
{
37-
Guard.NotNull(operation, nameof(operation));
3841
Guard.NotNull(source, nameof(source));
42+
Guard.NotNull(operation, nameof(operation));
43+
source.EnsureNotDisposed();
3944

4045
IInternalImageProcessingContext<TPixel> operationsRunner = source.GetConfiguration().ImageOperationsProvider
4146
.CreateImageProcessingContext(source, true);
@@ -51,8 +56,9 @@ public static void Mutate<TPixel>(this Image<TPixel> source, Action<IImageProces
5156
public static void Mutate<TPixel>(this Image<TPixel> source, params IImageProcessor[] operations)
5257
where TPixel : struct, IPixel<TPixel>
5358
{
54-
Guard.NotNull(operations, nameof(operations));
5559
Guard.NotNull(source, nameof(source));
60+
Guard.NotNull(operations, nameof(operations));
61+
source.EnsureNotDisposed();
5662

5763
IInternalImageProcessingContext<TPixel> operationsRunner = source.GetConfiguration().ImageOperationsProvider
5864
.CreateImageProcessingContext(source, true);
@@ -67,6 +73,10 @@ public static void Mutate<TPixel>(this Image<TPixel> source, params IImageProces
6773
/// <returns>The new <see cref="SixLabors.ImageSharp.Image"/>.</returns>
6874
public static Image Clone(this Image source, Action<IImageProcessingContext> operation)
6975
{
76+
Guard.NotNull(source, nameof(source));
77+
Guard.NotNull(operation, nameof(operation));
78+
source.EnsureNotDisposed();
79+
7080
ProcessingVisitor visitor = new ProcessingVisitor(operation, false);
7181
source.AcceptVisitor(visitor);
7282
return visitor.ResultImage;
@@ -82,8 +92,9 @@ public static Image Clone(this Image source, Action<IImageProcessingContext> ope
8292
public static Image<TPixel> Clone<TPixel>(this Image<TPixel> source, Action<IImageProcessingContext> operation)
8393
where TPixel : struct, IPixel<TPixel>
8494
{
85-
Guard.NotNull(operation, nameof(operation));
8695
Guard.NotNull(source, nameof(source));
96+
Guard.NotNull(operation, nameof(operation));
97+
source.EnsureNotDisposed();
8798

8899
IInternalImageProcessingContext<TPixel> operationsRunner = source.GetConfiguration().ImageOperationsProvider
89100
.CreateImageProcessingContext(source, false);
@@ -101,8 +112,9 @@ public static Image<TPixel> Clone<TPixel>(this Image<TPixel> source, Action<IIma
101112
public static Image<TPixel> Clone<TPixel>(this Image<TPixel> source, params IImageProcessor[] operations)
102113
where TPixel : struct, IPixel<TPixel>
103114
{
104-
Guard.NotNull(operations, nameof(operations));
105115
Guard.NotNull(source, nameof(source));
116+
Guard.NotNull(operations, nameof(operations));
117+
source.EnsureNotDisposed();
106118

107119
IInternalImageProcessingContext<TPixel> operationsRunner = source.GetConfiguration().ImageOperationsProvider
108120
.CreateImageProcessingContext(source, false);
@@ -152,4 +164,4 @@ public void Visit<TPixel>(Image<TPixel> image)
152164
}
153165
}
154166
}
155-
}
167+
}

tests/ImageSharp.Tests/Image/ImageCloneTests.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ namespace SixLabors.ImageSharp.Tests
77
{
88
public class ImageCloneTests
99
{
10+
[Fact]
11+
public void CloneAs_WhenDisposed_Throws()
12+
{
13+
Image<Rgba32> image = new Image<Rgba32>(5, 5);
14+
image.Dispose();
15+
16+
Assert.Throws<ObjectDisposedException>(() => image.CloneAs<Bgra32>());
17+
}
18+
19+
[Fact]
20+
public void Clone_WhenDisposed_Throws()
21+
{
22+
Image<Rgba32> image = new Image<Rgba32>(5, 5);
23+
image.Dispose();
24+
25+
Assert.Throws<ObjectDisposedException>(() => image.Clone());
26+
}
27+
1028
[Theory]
1129
[WithTestPatternImages(9, 9, PixelTypes.Rgba32)]
1230
public void CloneAs_ToBgra32(TestImageProvider<Rgba32> provider)
@@ -109,4 +127,4 @@ public void CloneAs_ToRgb24(TestImageProvider<Rgba32> provider)
109127
}
110128
}
111129
}
112-
}
130+
}

tests/ImageSharp.Tests/Image/ImageTests.Save.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
// ReSharper disable InconsistentNaming
55

66
using System;
7+
using System.IO;
8+
9+
using Moq;
10+
711
using SixLabors.ImageSharp.Formats.Png;
812
using SixLabors.ImageSharp.PixelFormats;
913
using Xunit;
@@ -65,6 +69,18 @@ public void SetEncoding()
6569
Assert.Equal("image/png", mime.DefaultMimeType);
6670
}
6771
}
72+
73+
[Fact]
74+
public void ThrowsWhenDisposed()
75+
{
76+
var image = new Image<Rgba32>(5, 5);
77+
image.Dispose();
78+
IImageEncoder encoder = Mock.Of<IImageEncoder>();
79+
using (MemoryStream stream = new MemoryStream())
80+
{
81+
Assert.Throws<ObjectDisposedException>(() => image.Save(stream, encoder));
82+
}
83+
}
6884
}
6985
}
7086
}

tests/ImageSharp.Tests/ImageOperationTests.cs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,22 @@ public class ImageOperationTests : IDisposable
2323
private readonly FakeImageOperationsProvider provider;
2424
private readonly IImageProcessor processorDefinition;
2525

26+
private static readonly string ExpectedExceptionMessage = GetExpectedExceptionText();
27+
2628
public ImageOperationTests()
2729
{
2830
this.provider = new FakeImageOperationsProvider();
2931

3032
Mock<IImageProcessor> processorMock = new Mock<IImageProcessor>();
3133
this.processorDefinition = processorMock.Object;
32-
34+
3335
this.image = new Image<Rgba32>(new Configuration
3436
{
3537
ImageOperationsProvider = this.provider
3638
}, 1, 1);
3739
}
3840

41+
3942
[Fact]
4043
public void MutateCallsImageOperationsProvider_Func_OriginalImage()
4144
{
@@ -109,5 +112,65 @@ public void ApplyProcessors_ListOfProcessors_AppliesAllProcessorsToOperation()
109112
}
110113

111114
public void Dispose() => this.image.Dispose();
115+
116+
[Fact]
117+
public void GenericMutate_WhenDisposed_Throws()
118+
{
119+
this.image.Dispose();
120+
121+
CheckThrowsCorrectObjectDisposedException(
122+
() => this.image.Mutate(x => x.ApplyProcessor(this.processorDefinition)));
123+
}
124+
125+
[Fact]
126+
public void GenericClone_WhenDisposed_Throws()
127+
{
128+
this.image.Dispose();
129+
130+
CheckThrowsCorrectObjectDisposedException(
131+
() => this.image.Clone(x => x.ApplyProcessor(this.processorDefinition)));
132+
}
133+
134+
[Fact]
135+
public void AgnosticMutate_WhenDisposed_Throws()
136+
{
137+
this.image.Dispose();
138+
Image img = this.image;
139+
140+
CheckThrowsCorrectObjectDisposedException(
141+
() => img.Mutate(x => x.ApplyProcessor(this.processorDefinition)));
142+
}
143+
144+
[Fact]
145+
public void AgnosticClone_WhenDisposed_Throws()
146+
{
147+
this.image.Dispose();
148+
Image img = this.image;
149+
150+
CheckThrowsCorrectObjectDisposedException(
151+
() => img.Clone(x => x.ApplyProcessor(this.processorDefinition)));
152+
}
153+
154+
private static string GetExpectedExceptionText()
155+
{
156+
try
157+
{
158+
Image<Rgba32> img = new Image<Rgba32>(1, 1);
159+
img.Dispose();
160+
img.EnsureNotDisposed();
161+
}
162+
catch (ObjectDisposedException ex)
163+
{
164+
return ex.Message;
165+
}
166+
167+
return "?";
168+
}
169+
170+
private static void CheckThrowsCorrectObjectDisposedException(Action action)
171+
{
172+
var ex = Assert.Throws<ObjectDisposedException>(action);
173+
Assert.Equal(ExpectedExceptionMessage, ex.Message);
174+
}
112175
}
113176
}

0 commit comments

Comments
 (0)