1 /*
2 * pluma-document-output-stream.c
3 * This file is part of pluma
4 *
5 * Copyright (C) 2010 - Ignacio Casal Quinteiro
6 * Copyright (C) 2012-2021 MATE Developers
7 *
8 * pluma is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * pluma is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with pluma; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin St, Fifth Floor,
21 * Boston, MA 02110-1301 USA
22 */
23
24 #include "config.h"
25
26 #include <string.h>
27 #include <glib.h>
28 #include <glib/gi18n.h>
29 #include <gio/gio.h>
30 #include "pluma-document-output-stream.h"
31
32 /* NOTE: never use async methods on this stream, the stream is just
33 * a wrapper around GtkTextBuffer api so that we can use GIO Stream
34 * methods, but the undelying code operates on a GtkTextBuffer, so
35 * there is no I/O involved and should be accessed only by the main
36 * thread */
37
38 #define MAX_UNICHAR_LEN 6
39
40 struct _PlumaDocumentOutputStreamPrivate
41 {
42 PlumaDocument *doc;
43 GtkTextIter pos;
44
45 gchar *buffer;
46 gsize buflen;
47
48 guint is_initialized : 1;
49 guint is_closed : 1;
50 };
51
52 enum
53 {
54 PROP_0,
55 PROP_DOCUMENT
56 };
57
58 G_DEFINE_TYPE_WITH_PRIVATE (PlumaDocumentOutputStream, pluma_document_output_stream, G_TYPE_OUTPUT_STREAM)
59
60 static gssize pluma_document_output_stream_write (GOutputStream *stream,
61 const void *buffer,
62 gsize count,
63 GCancellable *cancellable,
64 GError **error);
65
66 static gboolean pluma_document_output_stream_flush (GOutputStream *stream,
67 GCancellable *cancellable,
68 GError **error);
69
70 static gboolean pluma_document_output_stream_close (GOutputStream *stream,
71 GCancellable *cancellable,
72 GError **error);
73
74 static void
pluma_document_output_stream_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)75 pluma_document_output_stream_set_property (GObject *object,
76 guint prop_id,
77 const GValue *value,
78 GParamSpec *pspec)
79 {
80 PlumaDocumentOutputStream *stream = PLUMA_DOCUMENT_OUTPUT_STREAM (object);
81
82 switch (prop_id)
83 {
84 case PROP_DOCUMENT:
85 stream->priv->doc = PLUMA_DOCUMENT (g_value_get_object (value));
86 break;
87
88 default:
89 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
90 break;
91 }
92 }
93
94 static void
pluma_document_output_stream_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)95 pluma_document_output_stream_get_property (GObject *object,
96 guint prop_id,
97 GValue *value,
98 GParamSpec *pspec)
99 {
100 PlumaDocumentOutputStream *stream = PLUMA_DOCUMENT_OUTPUT_STREAM (object);
101
102 switch (prop_id)
103 {
104 case PROP_DOCUMENT:
105 g_value_set_object (value, stream->priv->doc);
106 break;
107
108 default:
109 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
110 break;
111 }
112 }
113
114 static void
pluma_document_output_stream_finalize(GObject * object)115 pluma_document_output_stream_finalize (GObject *object)
116 {
117 PlumaDocumentOutputStream *stream = PLUMA_DOCUMENT_OUTPUT_STREAM (object);
118
119 g_free (stream->priv->buffer);
120
121 G_OBJECT_CLASS (pluma_document_output_stream_parent_class)->finalize (object);
122 }
123
124 static void
pluma_document_output_stream_constructed(GObject * object)125 pluma_document_output_stream_constructed (GObject *object)
126 {
127 PlumaDocumentOutputStream *stream = PLUMA_DOCUMENT_OUTPUT_STREAM (object);
128
129 if (!stream->priv->doc)
130 {
131 g_critical ("This should never happen, a problem happened constructing the Document Output Stream!");
132 return;
133 }
134
135 /* Init the undoable action */
136 gtk_source_buffer_begin_not_undoable_action (GTK_SOURCE_BUFFER (stream->priv->doc));
137 /* clear the buffer */
138 gtk_text_buffer_set_text (GTK_TEXT_BUFFER (stream->priv->doc),
139 "", 0);
140 gtk_text_buffer_set_modified (GTK_TEXT_BUFFER (stream->priv->doc),
141 FALSE);
142
143 gtk_source_buffer_end_not_undoable_action (GTK_SOURCE_BUFFER (stream->priv->doc));
144 }
145
146 static void
pluma_document_output_stream_class_init(PlumaDocumentOutputStreamClass * klass)147 pluma_document_output_stream_class_init (PlumaDocumentOutputStreamClass *klass)
148 {
149 GObjectClass *object_class = G_OBJECT_CLASS (klass);
150 GOutputStreamClass *stream_class = G_OUTPUT_STREAM_CLASS (klass);
151
152 object_class->get_property = pluma_document_output_stream_get_property;
153 object_class->set_property = pluma_document_output_stream_set_property;
154 object_class->finalize = pluma_document_output_stream_finalize;
155 object_class->constructed = pluma_document_output_stream_constructed;
156
157 stream_class->write_fn = pluma_document_output_stream_write;
158 stream_class->flush = pluma_document_output_stream_flush;
159 stream_class->close_fn = pluma_document_output_stream_close;
160
161 g_object_class_install_property (object_class,
162 PROP_DOCUMENT,
163 g_param_spec_object ("document",
164 "Document",
165 "The document which is written",
166 PLUMA_TYPE_DOCUMENT,
167 G_PARAM_READWRITE |
168 G_PARAM_CONSTRUCT_ONLY));
169 }
170
171 static void
pluma_document_output_stream_init(PlumaDocumentOutputStream * stream)172 pluma_document_output_stream_init (PlumaDocumentOutputStream *stream)
173 {
174 stream->priv = pluma_document_output_stream_get_instance_private (stream);
175
176 stream->priv->buffer = NULL;
177 stream->priv->buflen = 0;
178
179 stream->priv->is_initialized = FALSE;
180 stream->priv->is_closed = FALSE;
181 }
182
183 static PlumaDocumentNewlineType
get_newline_type(GtkTextIter * end)184 get_newline_type (GtkTextIter *end)
185 {
186 PlumaDocumentNewlineType res;
187 GtkTextIter copy;
188 gunichar c;
189
190 copy = *end;
191 c = gtk_text_iter_get_char (©);
192
193 if (g_unichar_break_type (c) == G_UNICODE_BREAK_CARRIAGE_RETURN)
194 {
195 if (gtk_text_iter_forward_char (©) &&
196 g_unichar_break_type (gtk_text_iter_get_char (©)) == G_UNICODE_BREAK_LINE_FEED)
197 {
198 res = PLUMA_DOCUMENT_NEWLINE_TYPE_CR_LF;
199 }
200 else
201 {
202 res = PLUMA_DOCUMENT_NEWLINE_TYPE_CR;
203 }
204 }
205 else
206 {
207 res = PLUMA_DOCUMENT_NEWLINE_TYPE_LF;
208 }
209
210 return res;
211 }
212
213 GOutputStream *
pluma_document_output_stream_new(PlumaDocument * doc)214 pluma_document_output_stream_new (PlumaDocument *doc)
215 {
216 return G_OUTPUT_STREAM (g_object_new (PLUMA_TYPE_DOCUMENT_OUTPUT_STREAM,
217 "document", doc, NULL));
218 }
219
220 PlumaDocumentNewlineType
pluma_document_output_stream_detect_newline_type(PlumaDocumentOutputStream * stream)221 pluma_document_output_stream_detect_newline_type (PlumaDocumentOutputStream *stream)
222 {
223 PlumaDocumentNewlineType type;
224 GtkTextIter iter;
225
226 g_return_val_if_fail (PLUMA_IS_DOCUMENT_OUTPUT_STREAM (stream),
227 PLUMA_DOCUMENT_NEWLINE_TYPE_DEFAULT);
228
229 type = PLUMA_DOCUMENT_NEWLINE_TYPE_DEFAULT;
230
231 gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (stream->priv->doc),
232 &iter);
233
234 if (gtk_text_iter_ends_line (&iter) || gtk_text_iter_forward_to_line_end (&iter))
235 {
236 type = get_newline_type (&iter);
237 }
238
239 return type;
240 }
241
242 /* If the last char is a newline, remove it from the buffer (otherwise
243 GtkTextView shows it as an empty line). See bug #324942. */
244 static void
remove_ending_newline(PlumaDocumentOutputStream * stream)245 remove_ending_newline (PlumaDocumentOutputStream *stream)
246 {
247 GtkTextIter end;
248 GtkTextIter start;
249
250 gtk_text_buffer_get_end_iter (GTK_TEXT_BUFFER (stream->priv->doc), &end);
251 start = end;
252
253 gtk_text_iter_set_line_offset (&start, 0);
254
255 if (gtk_text_iter_ends_line (&start) &&
256 gtk_text_iter_backward_line (&start))
257 {
258 if (!gtk_text_iter_ends_line (&start))
259 {
260 gtk_text_iter_forward_to_line_end (&start);
261 }
262
263 /* Delete the empty line which is from 'start' to 'end' */
264 gtk_text_buffer_delete (GTK_TEXT_BUFFER (stream->priv->doc),
265 &start,
266 &end);
267 }
268 }
269
270 static void
end_append_text_to_document(PlumaDocumentOutputStream * stream)271 end_append_text_to_document (PlumaDocumentOutputStream *stream)
272 {
273 remove_ending_newline (stream);
274
275 gtk_text_buffer_set_modified (GTK_TEXT_BUFFER (stream->priv->doc),
276 FALSE);
277
278 gtk_source_buffer_end_not_undoable_action (GTK_SOURCE_BUFFER (stream->priv->doc));
279 }
280
281 static gssize
pluma_document_output_stream_write(GOutputStream * stream,const void * buffer,gsize count,GCancellable * cancellable,GError ** error)282 pluma_document_output_stream_write (GOutputStream *stream,
283 const void *buffer,
284 gsize count,
285 GCancellable *cancellable,
286 GError **error)
287 {
288 PlumaDocumentOutputStream *ostream;
289 gchar *text;
290 gsize len;
291 gboolean freetext = FALSE;
292 const gchar *end;
293 gboolean valid;
294
295 if (g_cancellable_set_error_if_cancelled (cancellable, error))
296 return -1;
297
298 ostream = PLUMA_DOCUMENT_OUTPUT_STREAM (stream);
299
300 if (!ostream->priv->is_initialized)
301 {
302 /* Init the undoable action */
303 gtk_source_buffer_begin_not_undoable_action (GTK_SOURCE_BUFFER (ostream->priv->doc));
304
305 gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (ostream->priv->doc),
306 &ostream->priv->pos);
307 ostream->priv->is_initialized = TRUE;
308 }
309
310 if (ostream->priv->buflen > 0)
311 {
312 len = ostream->priv->buflen + count;
313 text = g_new (gchar , len + 1);
314 memcpy (text, ostream->priv->buffer, ostream->priv->buflen);
315 memcpy (text + ostream->priv->buflen, buffer, count);
316 text[len] = '\0';
317 g_free (ostream->priv->buffer);
318 ostream->priv->buffer = NULL;
319 ostream->priv->buflen = 0;
320 freetext = TRUE;
321 }
322 else
323 {
324 text = (gchar *) buffer;
325 len = count;
326 }
327
328 /* validate */
329 valid = g_utf8_validate (text, len, &end);
330
331 /* Avoid keeping a CRLF across two buffers. */
332 if (valid && len > 1 && end[-1] == '\r')
333 {
334 valid = FALSE;
335 end--;
336 }
337
338 if (!valid)
339 {
340 gsize nvalid = end - text;
341 gsize remainder = len - nvalid;
342 gunichar ch;
343
344 if ((remainder < MAX_UNICHAR_LEN) &&
345 ((ch = g_utf8_get_char_validated (text + nvalid, remainder)) == (gunichar)-2 ||
346 ch == (gunichar)'\r'))
347 {
348 ostream->priv->buffer = g_strndup (end, remainder);
349 ostream->priv->buflen = remainder;
350 len -= remainder;
351 }
352 else
353 {
354 /* TODO: we could escape invalid text and tag it in red
355 * and make the doc readonly.
356 */
357 g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
358 _("Invalid UTF-8 sequence in input"));
359
360 if (freetext)
361 g_free (text);
362
363 return -1;
364 }
365 }
366
367 gtk_text_buffer_insert (GTK_TEXT_BUFFER (ostream->priv->doc),
368 &ostream->priv->pos, text, len);
369
370 if (freetext)
371 g_free (text);
372
373 return count;
374 }
375
376 static gboolean
pluma_document_output_stream_flush(GOutputStream * stream,GCancellable * cancellable,GError ** error)377 pluma_document_output_stream_flush (GOutputStream *stream,
378 GCancellable *cancellable,
379 GError **error)
380 {
381 PlumaDocumentOutputStream *ostream = PLUMA_DOCUMENT_OUTPUT_STREAM (stream);
382
383 /* Flush deferred data if some. */
384 if (!ostream->priv->is_closed && ostream->priv->is_initialized &&
385 ostream->priv->buflen > 0 &&
386 pluma_document_output_stream_write (stream, "", 0, cancellable,
387 error) == -1)
388 return FALSE;
389
390 return TRUE;
391 }
392
393 static gboolean
pluma_document_output_stream_close(GOutputStream * stream,GCancellable * cancellable,GError ** error)394 pluma_document_output_stream_close (GOutputStream *stream,
395 GCancellable *cancellable,
396 GError **error)
397 {
398 PlumaDocumentOutputStream *ostream = PLUMA_DOCUMENT_OUTPUT_STREAM (stream);
399
400 if (!ostream->priv->is_closed && ostream->priv->is_initialized)
401 {
402 end_append_text_to_document (ostream);
403 ostream->priv->is_closed = TRUE;
404 }
405
406 if (ostream->priv->buflen > 0)
407 {
408 g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
409 _("Incomplete UTF-8 sequence in input"));
410 return FALSE;
411 }
412
413 return TRUE;
414 }
415