# HG changeset patch
# Node ID 5ebcf43193cb4bec8ab2bf79ecf8cd5a8bd1f3f3
# Parent  9db841a679c381b58818611cb607e3c223595221
diff --git a/birb/birbstringstream.c b/birb/birbstringstream.c
new file mode 100644
--- /dev/null
+++ b/birb/birbstringstream.c
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023-2025 Birb Developers
+ *
+ * Birb is the legal property of its developers, whose names are too
+ * numerous to list here. Please refer to the AUTHORS file distributed
+ * with this source distribution
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "birbstringstream.h"
+
+struct _BirbStringStream {
+	GInputStream parent;
+
+	GString *string;
+};
+
+enum {
+	PROP_0,
+	PROP_STRING,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+/******************************************************************************
+ * GInputStream Implementation
+ *****************************************************************************/
+static gssize
+birb_string_stream_read_fn(GInputStream *input,
+                           gpointer buffer,
+                           gsize count,
+                           G_GNUC_UNUSED GCancellable *cancellable,
+                           G_GNUC_UNUSED GError **error)
+{
+	BirbStringStream *stream = BIRB_STRING_STREAM(input);
+	gssize bytes = 0;
+
+	bytes = MIN(stream->string->len, count);
+	if(bytes > 0) {
+		strncpy(buffer, stream->string->str, bytes);
+		g_string_erase(stream->string, 0, bytes);
+	}
+
+	return bytes;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_FINAL_TYPE(BirbStringStream, birb_string_stream, G_TYPE_INPUT_STREAM)
+
+static void
+birb_string_stream_finalize(GObject *obj) {
+	BirbStringStream *stream = BIRB_STRING_STREAM(obj);
+
+	if(stream->string != NULL) {
+		g_string_free(stream->string, TRUE);
+		stream->string = NULL;
+	}
+
+	G_OBJECT_CLASS(birb_string_stream_parent_class)->finalize(obj);
+}
+
+static void
+birb_string_stream_get_property(GObject *obj, guint param_id, GValue *value,
+                                GParamSpec *pspec)
+{
+	BirbStringStream *stream = BIRB_STRING_STREAM(obj);
+
+	switch(param_id) {
+	case PROP_STRING:
+		g_value_set_pointer(value, birb_string_stream_get_string(stream));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+birb_string_stream_init(BirbStringStream *stream) {
+	stream->string = g_string_new("");
+}
+
+static void
+birb_string_stream_class_init(BirbStringStreamClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+	GInputStreamClass *input_stream_class = G_INPUT_STREAM_CLASS(klass);
+
+	obj_class->finalize = birb_string_stream_finalize;
+	obj_class->get_property = birb_string_stream_get_property;
+
+	input_stream_class->read_fn = birb_string_stream_read_fn;
+
+	/**
+	 * BirbStringStream:string:
+	 *
+	 * The string backing this stream.
+	 *
+	 * Since: 0.4
+	 */
+	properties[PROP_STRING] = g_param_spec_pointer(
+		"string", NULL, NULL,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+GInputStream *
+birb_string_stream_new(void) {
+	return g_object_new(BIRB_TYPE_STRING_STREAM, NULL);
+}
+
+GString *
+birb_string_stream_get_string(BirbStringStream *stream) {
+	g_return_val_if_fail(BIRB_IS_STRING_STREAM(stream), NULL);
+
+	return stream->string;
+}
diff --git a/birb/birbstringstream.h b/birb/birbstringstream.h
new file mode 100644
--- /dev/null
+++ b/birb/birbstringstream.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023-2025 Birb Developers
+ *
+ * Birb is the legal property of its developers, whose names are too
+ * numerous to list here. Please refer to the AUTHORS file distributed
+ * with this source distribution
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(BIRB_GLOBAL_HEADER_INSIDE) && !defined(BIRB_COMPILATION)
+# error "only <birb.h> may be included directly"
+#endif
+
+#ifndef BIRB_STRING_STREAM_H
+#define BIRB_STRING_STREAM_H
+
+#include <gio/gio.h>
+
+#include "birbversion.h"
+
+G_BEGIN_DECLS
+
+#define BIRB_TYPE_STRING_STREAM (birb_string_stream_get_type())
+
+/**
+ * BirbStringStream:
+ *
+ * A [class@Gio.InputStream] that is backed by a [type@GLib.String].
+ *
+ * Since: 0.4
+ */
+BIRB_AVAILABLE_IN_0_4
+G_DECLARE_FINAL_TYPE(BirbStringStream, birb_string_stream, BIRB, STRING_STREAM,
+                     GInputStream)
+
+/**
+ * birb_string_stream_new:
+ *
+ * Creates a new filter using @match_func to determine what to filter.
+ *
+ * If @match_func is %NULL, the filter will match all items.
+ *
+ * Returns: (transfer full) (type BirbStringStream): The new instance.
+ *
+ * Since: 0.4
+ */
+BIRB_AVAILABLE_IN_0_4
+GInputStream *birb_string_stream_new(void);
+
+/**
+ * birb_string_stream_get_string:
+ *
+ * Gets the string backing this stream.
+ *
+ * Returns: (transfer none): The string.
+ *
+ * Since: 0.4
+ */
+BIRB_AVAILABLE_IN_0_4
+GString *birb_string_stream_get_string(BirbStringStream *stream);
+
+G_END_DECLS
+
+#endif /* BIRB_STRING_STREAM_H */
diff --git a/birb/meson.build b/birb/meson.build
--- a/birb/meson.build
+++ b/birb/meson.build
@@ -8,6 +8,7 @@
   'birbfilter.c',
   'birbqueuedoutputstream.c',
   'birbstringobject.c',
+  'birbstringstream.c',
   'birbversion.c',
 ]
 
@@ -20,6 +21,7 @@
   'birbfilter.h',
   'birbqueuedoutputstream.h',
   'birbstringobject.h',
+  'birbstringstream.h',
   'birbversion.h',
 ]
 
diff --git a/birb/tests/meson.build b/birb/tests/meson.build
--- a/birb/tests/meson.build
+++ b/birb/tests/meson.build
@@ -6,6 +6,7 @@
   'filter',
   'queued_output_stream',
   'string_object',
+  'string_stream',
 ]
 
 foreach PROGRAM : PROGRAMS
diff --git a/birb/tests/test_string_stream.c b/birb/tests/test_string_stream.c
new file mode 100644
--- /dev/null
+++ b/birb/tests/test_string_stream.c
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2023-2025 Birb Developers
+ *
+ * Birb is the legal property of its developers, whose names are too
+ * numerous to list here. Please refer to the AUTHORS file distributed
+ * with this source distribution
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
+ */
+
+#include <glib.h>
+
+#include <birb.h>
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_string_stream_new(void) {
+	GInputStream *stream = NULL;
+
+	stream = birb_string_stream_new();
+	birb_assert_type(stream, BIRB_TYPE_STRING_STREAM);
+
+	g_assert_finalize_object(stream);
+}
+
+static void
+test_string_stream_properties(void) {
+	GInputStream *stream = NULL;
+	GString *string = NULL;
+
+	stream = g_object_new(
+		BIRB_TYPE_STRING_STREAM,
+		NULL);
+
+	birb_assert_type(stream, BIRB_TYPE_STRING_STREAM);
+
+	g_object_get(
+		G_OBJECT(stream),
+		"string", &string,
+		NULL);
+
+	g_assert_nonnull(string);
+
+	g_assert_finalize_object(stream);
+}
+
+static void
+test_string_stream_read(void) {
+	GError *error = NULL;
+	GInputStream *input = NULL;
+	GString *string = NULL;
+	char buffer[255];
+	gssize bytes = 0;
+
+	input = birb_string_stream_new();
+	string = birb_string_stream_get_string(BIRB_STRING_STREAM(input));
+
+	/* Make sure we return 0 when there's no text to read. */
+	bytes = g_input_stream_read(input, buffer, 255, NULL, &error);
+	g_assert_no_error(error);
+	g_assert_cmpint(bytes, ==, 0);
+
+	/* Write 'Hello World!' to the string and make sure we can read it back. */
+	g_string_append(string, "Hello World!");
+	bytes = g_input_stream_read(input, buffer, 255, NULL, &error);
+	g_assert_no_error(error);
+	g_assert_cmpint(bytes, ==, strlen("Hello World!"));
+	buffer[bytes] = '\0';
+	g_assert_cmpstr(buffer, ==, "Hello World!");
+
+	/* Read again and make sure we got nothing. */
+	bytes = g_input_stream_read(input, buffer, 255, NULL, &error);
+	g_assert_no_error(error);
+	g_assert_cmpint(bytes, ==, 0);
+
+	/* Now read 'Hello World!' but in multiple parts. */
+	g_string_append(string, "Hello World!");
+	bytes = g_input_stream_read(input, buffer, 6, NULL, &error);
+	g_assert_no_error(error);
+	g_assert_cmpint(bytes, ==, 6);
+	buffer[bytes] = '\0';
+	g_assert_cmpstr(buffer, ==, "Hello ");
+
+	bytes = g_input_stream_read(input, buffer, 255, NULL, &error);
+	g_assert_no_error(error);
+	g_assert_cmpint(bytes, ==, 6);
+	buffer[bytes] = '\0';
+	g_assert_cmpstr(buffer, ==, "World!");
+
+	g_assert_finalize_object(input);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char **argv) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_set_nonfatal_assertions();
+
+	g_test_add_func("/string-stream/new", test_string_stream_new);
+	g_test_add_func("/string-stream/properties",
+	                test_string_stream_properties);
+
+	g_test_add_func("/string-stream/read", test_string_stream_read);
+
+	return g_test_run();
+}
