[libsoup/content-sniffing] Ground work for content sniffing capabilities



commit 6718d22e46f66edbc30689bb0a32a781a8086426
Author: Gustavo Noronha Silva <gns gnome org>
Date:   Wed Jun 17 20:53:17 2009 -0300

    Ground work for content sniffing capabilities
    
    This is the initial work required to implement content sniffing in
    libsoup. This first commit provides the new SoupContentSniffer
    feature, and hooks it into the message IO code.

 libsoup/Makefile.am            |    2 +
 libsoup/soup-content-sniffer.c |  146 ++++++++++++++++++++++++++
 libsoup/soup-content-sniffer.h |   56 ++++++++++
 libsoup/soup-marshal.list      |    1 +
 libsoup/soup-message-io.c      |  130 ++++++++++++++++++++++-
 libsoup/soup-message-private.h |    5 +
 libsoup/soup-message.c         |   54 ++++++++++
 libsoup/soup-message.h         |    1 +
 libsoup/soup.h                 |    1 +
 tests/Makefile.am              |    2 +
 tests/resources/mbox           |   16 +++
 tests/sniffing-test.c          |  227 ++++++++++++++++++++++++++++++++++++++++
 12 files changed, 637 insertions(+), 4 deletions(-)
---
diff --git a/libsoup/Makefile.am b/libsoup/Makefile.am
index 949f243..2d3a6ea 100644
--- a/libsoup/Makefile.am
+++ b/libsoup/Makefile.am
@@ -55,6 +55,7 @@ soup_headers =			\
 	soup-auth-domain.h	\
 	soup-auth-domain-basic.h  \
 	soup-auth-domain-digest.h \
+	soup-content-sniffer.h  \
 	soup-cookie.h		\
 	soup-cookie-jar.h	\
 	soup-cookie-jar-text.h	\
@@ -119,6 +120,7 @@ libsoup_2_4_la_SOURCES =		\
 	soup-auth-manager-ntlm.c	\
 	soup-connection.h		\
 	soup-connection.c		\
+	soup-content-sniffer.c		\
 	soup-cookie.c			\
 	soup-cookie-jar.c		\
 	soup-cookie-jar-text.c		\
diff --git a/libsoup/soup-content-sniffer.c b/libsoup/soup-content-sniffer.c
new file mode 100644
index 0000000..5ce0644
--- /dev/null
+++ b/libsoup/soup-content-sniffer.c
@@ -0,0 +1,146 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-content-sniffer.c
+ *
+ * Copyright (C) 2009 Gustavo Noronha Silva.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <string.h>
+#include <gio/gio.h>
+
+#include "soup-content-sniffer.h"
+#include "soup-enum-types.h"
+#include "soup-message.h"
+#include "soup-message-private.h"
+#include "soup-session-feature.h"
+#include "soup-uri.h"
+
+/**
+ * SECTION:soup-content-sniffer
+ * @short_description: Content sniffing for #SoupSession
+ *
+ * A #SoupContentSniffer tries to detect the actual content type of
+ * the files that are being downloaded by looking at some of the data
+ * before the #SoupMessage emits its #SoupMessage::got-headers signal.
+ * #SoupContentSniffer implements #SoupSessionFeature, so you can add
+ * content sniffing to a session with soup_session_add_feature() or
+ * soup_session_add_feature_by_type().
+ *
+ * Since: 2.27.3
+ **/
+
+static char* sniff (SoupContentSniffer *sniffer, SoupMessage *msg, SoupBuffer *buffer, gboolean *uncertain);
+static gsize get_buffer_size (SoupContentSniffer *sniffer);
+
+static void soup_content_sniffer_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
+
+static void request_queued (SoupSessionFeature *feature, SoupSession *session, SoupMessage *msg);
+static void request_unqueued (SoupSessionFeature *feature, SoupSession *session, SoupMessage *msg);
+
+G_DEFINE_TYPE_WITH_CODE (SoupContentSniffer, soup_content_sniffer, G_TYPE_OBJECT,
+			 G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
+						soup_content_sniffer_session_feature_init))
+
+static void
+soup_content_sniffer_init (SoupContentSniffer *content_sniffer)
+{
+}
+
+static void
+soup_content_sniffer_class_init (SoupContentSnifferClass *content_sniffer_class)
+{
+	content_sniffer_class->sniff = sniff;
+	content_sniffer_class->get_buffer_size = get_buffer_size;
+}
+
+static void
+soup_content_sniffer_session_feature_init (SoupSessionFeatureInterface *feature_interface,
+					   gpointer interface_data)
+{
+	feature_interface->request_queued = request_queued;
+	feature_interface->request_unqueued = request_unqueued;
+}
+
+/**
+ * soup_content_sniffer_new:
+ *
+ * Creates a new #SoupContentSniffer.
+ *
+ * Returns: a new #SoupContentSniffer
+ *
+ * Since: 2.27.3
+ **/
+SoupContentSniffer *
+soup_content_sniffer_new ()
+{
+	return g_object_new (SOUP_TYPE_CONTENT_SNIFFER, NULL);
+}
+
+static char*
+sniff (SoupContentSniffer *sniffer, SoupMessage *msg, SoupBuffer *buffer, gboolean *uncertain)
+{
+	SoupURI *uri;
+	char *uri_path;
+	char *content_type;
+	char *mime_type;
+
+	uri = soup_message_get_uri (msg);
+	uri_path = soup_uri_to_string (uri, TRUE);
+
+	content_type= g_content_type_guess (uri_path, (const guchar*)buffer->data, buffer->length, uncertain);
+	mime_type = g_content_type_get_mime_type (content_type);
+
+	g_free (uri_path);
+	g_free (content_type);
+
+	return mime_type;
+}
+
+static gsize
+get_buffer_size (SoupContentSniffer *sniffer)
+{
+	return 512;
+}
+
+static void
+soup_content_sniffer_got_headers_cb (SoupMessage *msg, SoupContentSniffer *sniffer)
+{
+	SoupMessagePrivate *priv = SOUP_MESSAGE_GET_PRIVATE (msg);
+	SoupContentSnifferClass *content_sniffer_class = SOUP_CONTENT_SNIFFER_GET_CLASS (sniffer);
+	const char *content_type = soup_message_headers_get_content_type (msg->response_headers, NULL);
+
+	if ((content_type == NULL)
+	    || (strcmp (content_type, "application/octet-stream") == 0)
+	    || (strcmp (content_type, "text/plain") == 0)) {
+		priv->should_sniff_content = TRUE;
+		priv->bytes_for_sniffing = content_sniffer_class->get_buffer_size (sniffer);
+	}
+}
+
+static void
+request_queued (SoupSessionFeature *feature, SoupSession *session,
+		SoupMessage *msg)
+{
+	SoupMessagePrivate *priv = SOUP_MESSAGE_GET_PRIVATE (msg);
+
+	priv->sniffer = g_object_ref (feature);
+	g_signal_connect (msg, "got-headers",
+			  G_CALLBACK (soup_content_sniffer_got_headers_cb),
+			  feature);
+}
+
+static void
+request_unqueued (SoupSessionFeature *feature, SoupSession *session,
+		  SoupMessage *msg)
+{
+	SoupMessagePrivate *priv = SOUP_MESSAGE_GET_PRIVATE (msg);
+
+	g_object_unref (priv->sniffer);
+	priv->sniffer = NULL;
+
+	g_signal_handlers_disconnect_by_func (msg, soup_content_sniffer_got_headers_cb, feature);
+}
diff --git a/libsoup/soup-content-sniffer.h b/libsoup/soup-content-sniffer.h
new file mode 100644
index 0000000..77123ed
--- /dev/null
+++ b/libsoup/soup-content-sniffer.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2009 Gustavo Noronha Silva.
+ */
+
+#ifndef SOUP_CONTENT_SNIFFER_H
+#define SOUP_CONTENT_SNIFFER_H 1
+
+#include <libsoup/soup-types.h>
+#include <libsoup/soup-message-body.h>
+
+G_BEGIN_DECLS
+
+#define SOUP_TYPE_CONTENT_SNIFFER            (soup_content_sniffer_get_type ())
+#define SOUP_CONTENT_SNIFFER(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), SOUP_TYPE_CONTENT_SNIFFER, SoupContentSniffer))
+#define SOUP_CONTENT_SNIFFER_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_CONTENT_SNIFFER, SoupContentSnifferClass))
+#define SOUP_IS_CONTENT_SNIFFER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SOUP_TYPE_CONTENT_SNIFFER))
+#define SOUP_IS_CONTENT_SNIFFER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((obj), SOUP_TYPE_CONTENT_SNIFFER))
+#define SOUP_CONTENT_SNIFFER_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_CONTENT_SNIFFER, SoupContentSnifferClass))
+
+typedef struct _SoupContentSnifferPrivate SoupContentSnifferPrivate;
+
+typedef struct {
+	GObject parent;
+
+	SoupContentSnifferPrivate *priv;
+} SoupContentSniffer;
+
+typedef struct {
+	GObjectClass parent_class;
+
+	char* (*sniff)              (SoupContentSniffer *sniffer,
+				     SoupMessage *msg,
+				     SoupBuffer *buffer,
+				     gboolean *uncertain);
+	gsize (*get_buffer_size)    (SoupContentSniffer *sniffer);
+
+	/* Padding for future expansion */
+	void (*_libsoup_reserved1) (void);
+	void (*_libsoup_reserved2) (void);
+	void (*_libsoup_reserved3) (void);
+	void (*_libsoup_reserved4) (void);
+	void (*_libsoup_reserved5) (void);
+} SoupContentSnifferClass;
+
+GType               soup_content_sniffer_get_type (void);
+
+SoupContentSniffer *soup_content_sniffer_new      (void);
+
+void                soup_content_sniffer_sniff    (SoupContentSniffer *sniffer,
+						   SoupMessage *msg,
+						   SoupBuffer *buffer);
+
+G_END_DECLS
+
+#endif /* SOUP_CONTENT_SNIFFER_H */
diff --git a/libsoup/soup-marshal.list b/libsoup/soup-marshal.list
index 1a43570..785fd6f 100644
--- a/libsoup/soup-marshal.list
+++ b/libsoup/soup-marshal.list
@@ -6,3 +6,4 @@ NONE:OBJECT,OBJECT
 NONE:OBJECT,POINTER
 NONE:BOXED,BOXED
 NONE:OBJECT,OBJECT,BOOLEAN
+NONE:STRING
diff --git a/libsoup/soup-message-io.c b/libsoup/soup-message-io.c
index 8e04b66..8c29acc 100644
--- a/libsoup/soup-message-io.c
+++ b/libsoup/soup-message-io.c
@@ -18,6 +18,7 @@
 #include "soup-misc.h"
 #include "soup-socket.h"
 #include "soup-ssl.h"
+#include "soup-uri.h"
 
 typedef enum {
 	SOUP_MESSAGE_IO_CLIENT,
@@ -53,6 +54,11 @@ typedef struct {
 	SoupMessageBody      *read_body;
 	goffset               read_length;
 
+	gboolean              acked_content_sniff_decision;
+	gboolean              delay_got_chunks;
+	SoupMessageBody      *delayed_chunk_data;
+	gsize                 delayed_chunk_length;
+
 	SoupMessageIOState    write_state;
 	SoupEncoding          write_encoding;
 	GString              *write_buf;
@@ -105,6 +111,9 @@ soup_message_io_cleanup (SoupMessage *msg)
 	if (io->write_chunk)
 		soup_buffer_free (io->write_chunk);
 
+	if (io->delayed_chunk_data)
+		soup_message_body_free (io->delayed_chunk_data);
+
 	g_slice_free (SoupMessageIOData, io);
 }
 
@@ -207,6 +216,38 @@ io_disconnected (SoupSocket *sock, SoupMessage *msg)
 	io_error (sock, msg, NULL);
 }
 
+static gboolean
+io_sniff_content (SoupMessage *msg)
+{
+	SoupMessagePrivate *priv = SOUP_MESSAGE_GET_PRIVATE (msg);
+	SoupMessageIOData *io = priv->io_data;
+	SoupBuffer *sniffed_buffer = soup_message_body_flatten (io->delayed_chunk_data);
+	SoupContentSnifferClass *content_sniffer_class = SOUP_CONTENT_SNIFFER_GET_CLASS (priv->sniffer);
+	char *sniffed_mime_type;
+	gboolean uncertain;
+
+	io->delay_got_chunks = FALSE;
+
+	sniffed_mime_type = content_sniffer_class->sniff (priv->sniffer, msg, sniffed_buffer, &uncertain);
+	if (!uncertain) {
+		SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
+		soup_message_content_sniffed (msg, sniffed_mime_type);
+		g_free (sniffed_mime_type);
+		sniffed_mime_type = NULL;
+		SOUP_MESSAGE_IO_RETURN_VAL_IF_CANCELLED_OR_PAUSED (FALSE);
+	}
+	g_free (sniffed_mime_type);
+
+	SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
+	soup_message_got_chunk (msg, sniffed_buffer);
+	soup_buffer_free (sniffed_buffer);
+	soup_message_body_free (io->delayed_chunk_data);
+	io->delayed_chunk_data = NULL;
+	SOUP_MESSAGE_IO_RETURN_VAL_IF_CANCELLED_OR_PAUSED (FALSE);
+
+	return TRUE;
+}
+
 /* Reads data from io->sock into io->read_meta_buf. If @to_blank is
  * %TRUE, it reads up until a blank line ("CRLF CRLF" or "LF LF").
  * Otherwise, it reads up until a single CRLF or LF.
@@ -294,6 +335,21 @@ read_body_chunk (SoupMessage *msg)
 	GError *error = NULL;
 	SoupBuffer *buffer;
 
+	if (!io->acked_content_sniff_decision) {
+		/* The content sniffer feature decides whether a
+		 * message needs to be sniffed while handling
+		 * got-headers, but the message may be paused in a
+		 * user handler, so we need to make sure the signal is
+		 * emitted, or delay_got_chunks is correctly setup
+		 * here.
+		 */
+		if (priv->should_sniff_content)
+			io->delay_got_chunks = TRUE;
+		else if (priv->sniffer)
+			soup_message_content_sniffed(msg, NULL);
+		io->acked_content_sniff_decision = TRUE;
+	}
+
 	while (read_to_eof || io->read_length > 0) {
 		if (priv->chunk_allocator) {
 			buffer = priv->chunk_allocator (msg, io->read_length, priv->chunk_allocator_data);
@@ -324,10 +380,24 @@ read_body_chunk (SoupMessage *msg)
 
 			io->read_length -= nread;
 
-			SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
-			soup_message_got_chunk (msg, buffer);
-			soup_buffer_free (buffer);
-			SOUP_MESSAGE_IO_RETURN_VAL_IF_CANCELLED_OR_PAUSED (FALSE);
+			if (io->delay_got_chunks) {
+				if (!io->delayed_chunk_data)
+					io->delayed_chunk_data = soup_message_body_new ();
+
+				soup_message_body_append_buffer (io->delayed_chunk_data, buffer);
+				io->delayed_chunk_length += buffer->length;
+
+				/* We already have enough data to perform sniffing, so do it */
+				if (io->delayed_chunk_length > priv->bytes_for_sniffing) {
+					if (!io_sniff_content (msg))
+						return FALSE;
+				}
+			} else {
+				SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
+				soup_message_got_chunk (msg, buffer);
+				soup_buffer_free (buffer);
+				SOUP_MESSAGE_IO_RETURN_VAL_IF_CANCELLED_OR_PAUSED (FALSE);
+			}
 			continue;
 		}
 
@@ -675,6 +745,23 @@ io_read (SoupSocket *sock, SoupMessage *msg)
 	guint status;
 
  read_more:
+	/* We have delayed chunks, but are no longer delaying, so this
+	 * means we already sniffed but the message got paused while
+	 * content-sniffed was being handled, in which case we did not
+	 * emit the necessary got-chunk; See also the handling for
+	 * state SOUP_MESSAGE_IO_STATE_BODY in the switch bellow.
+	 */
+	if (io->delayed_chunk_data && !io->delay_got_chunks) {
+		SoupBuffer *sniffed_buffer = soup_message_body_flatten (io->delayed_chunk_data);
+
+		SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
+		soup_message_got_chunk (msg, sniffed_buffer);
+		soup_buffer_free (sniffed_buffer);
+		soup_message_body_free (io->delayed_chunk_data);
+		io->delayed_chunk_data = NULL;
+		SOUP_MESSAGE_IO_RETURN_IF_CANCELLED_OR_PAUSED;
+	}
+
 	switch (io->read_state) {
 	case SOUP_MESSAGE_IO_STATE_NOT_STARTED:
 		return;
@@ -782,6 +869,38 @@ io_read (SoupSocket *sock, SoupMessage *msg)
 			return;
 
 	got_body:
+		/* A chunk of data may have been read and the emission
+		 * of got_chunk delayed because we wanted to wait for
+		 * more chunks to arrive, for doing content sniffing,
+		 * but the body was too small, so we need to check if
+		 * an emission is in order here, along with the
+		 * sniffing, if we haven't done it yet, of course.
+		 */
+		if (io->delayed_chunk_data) {
+			if (io->delay_got_chunks) {
+				if (!io_sniff_content (msg))
+					return;
+			} else {
+				SoupBuffer *sniffed_buffer = soup_message_body_flatten (io->delayed_chunk_data);
+
+				SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
+				soup_message_got_chunk (msg, sniffed_buffer);
+				soup_buffer_free (sniffed_buffer);
+				soup_message_body_free (io->delayed_chunk_data);
+				io->delayed_chunk_data = NULL;
+
+				/* If we end up returning, read_state
+				 * needs to be set to IO_STATE_BODY,
+				 * and read_length must be 0; since we
+				 * may be coming from STATE_TRAILERS,
+				 * or may be doing a read-to-eof, we
+				 * sanitize these here. */
+				io->read_state = SOUP_MESSAGE_IO_STATE_BODY;
+				io->read_length = 0;
+				SOUP_MESSAGE_IO_RETURN_IF_CANCELLED_OR_PAUSED;
+			}
+		}
+
 		io->read_state = SOUP_MESSAGE_IO_STATE_FINISHING;
 
 		SOUP_MESSAGE_IO_PREPARE_FOR_CALLBACK;
@@ -885,6 +1004,9 @@ new_iostate (SoupMessage *msg, SoupSocket *sock, SoupMessageIOMode mode,
 	io->read_state  = SOUP_MESSAGE_IO_STATE_NOT_STARTED;
 	io->write_state = SOUP_MESSAGE_IO_STATE_NOT_STARTED;
 
+	if (priv->should_sniff_content)
+		io->delay_got_chunks = TRUE;
+
 	if (priv->io_data)
 		soup_message_io_cleanup (msg);
 	priv->io_data = io;
diff --git a/libsoup/soup-message-private.h b/libsoup/soup-message-private.h
index f47251a..999c335 100644
--- a/libsoup/soup-message-private.h
+++ b/libsoup/soup-message-private.h
@@ -9,6 +9,7 @@
 #include "soup-message.h"
 #include "soup-auth.h"
 #include "soup-connection.h"
+#include "soup-content-sniffer.h"
 
 typedef enum {
 	SOUP_MESSAGE_IO_STATUS_IDLE,
@@ -29,6 +30,10 @@ typedef struct {
 	guint              msg_flags;
 	gboolean           server_side;
 
+	SoupContentSniffer *sniffer;
+	gboolean           should_sniff_content;
+	gsize              bytes_for_sniffing;
+
 	SoupHTTPVersion    http_version, orig_http_version;
 
 	SoupURI           *uri;
diff --git a/libsoup/soup-message.c b/libsoup/soup-message.c
index 5475bb7..cc3a5d7 100644
--- a/libsoup/soup-message.c
+++ b/libsoup/soup-message.c
@@ -99,6 +99,7 @@ enum {
 	GOT_HEADERS,
 	GOT_CHUNK,
 	GOT_BODY,
+	CONTENT_SNIFFED,
 
 	RESTARTED,
 	FINISHED,
@@ -402,6 +403,42 @@ soup_message_class_init (SoupMessageClass *message_class)
 			      G_TYPE_NONE, 0);
 
 	/**
+	 * SoupMessage::content-sniffed:
+	 * @msg: the message
+	 * @type: the content type that we got from sniffing
+	 *
+	 * This signal is emitted after %got-headers, and before the
+	 * first %got-chunk. If content sniffing is disabled, or no
+	 * content sniffing will be performed, due to the sniffer
+	 * deciding to trust the Content-Type sent by the server, this
+	 * signal is emitted immediately after %got_headers, and @type
+	 * is %NULL.
+	 *
+	 * If the #SoupContentSniffer feature is enabled, and the
+	 * sniffer decided to perform sniffing, the first %got_chunk
+	 * emission may be delayed, so that the sniffer has enough
+	 * data to correctly sniff the content. It notified the
+	 * library user that the content has been sniffed, and allows
+	 * it to change the header contents in the message, if
+	 * desired.
+	 *
+	 * After this signal is emitted, the data that was spooled so
+	 * that sniffing could be done is delivered on the first
+	 * emission of %got_chunk.
+	 *
+	 * Since: 2.27.3
+	 **/
+	signals[CONTENT_SNIFFED] =
+		g_signal_new ("content_sniffed",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_FIRST,
+			      0,
+			      NULL, NULL,
+			      soup_marshal_NONE__STRING,
+			      G_TYPE_NONE, 1,
+			      G_TYPE_STRING);
+
+	/**
 	 * SoupMessage::restarted:
 	 * @msg: the message
 	 *
@@ -858,6 +895,23 @@ soup_message_got_body (SoupMessage *msg)
 	g_signal_emit (msg, signals[GOT_BODY], 0);
 }
 
+/**
+ * soup_message_content_sniffed:
+ * @msg: a #SoupMessage
+ * @type: a string with the sniffed content type
+ *
+ * Emits the %content_sniffed signal, indicating that the IO layer
+ * finished sniffing the content type for @msg. If content sniffing
+ * will not be performed, due to the sniffer deciding to trust the
+ * Content-Type sent by the server, this signal is emitted immediately
+ * after %got_headers, with %NULL as @content_type.
+ **/
+void
+soup_message_content_sniffed (SoupMessage *msg, const char *content_type)
+{
+	g_signal_emit (msg, signals[CONTENT_SNIFFED], 0, content_type);
+}
+
 static void
 restarted (SoupMessage *req)
 {
diff --git a/libsoup/soup-message.h b/libsoup/soup-message.h
index 1b850be..4a51466 100644
--- a/libsoup/soup-message.h
+++ b/libsoup/soup-message.h
@@ -155,6 +155,7 @@ void soup_message_got_informational   (SoupMessage *msg);
 void soup_message_got_headers         (SoupMessage *msg);
 void soup_message_got_chunk           (SoupMessage *msg, SoupBuffer *chunk);
 void soup_message_got_body            (SoupMessage *msg);
+void soup_message_content_sniffed     (SoupMessage *msg, const char *content_type);
 void soup_message_restarted           (SoupMessage *msg);
 void soup_message_finished            (SoupMessage *msg);
 
diff --git a/libsoup/soup.h b/libsoup/soup.h
index 496a4c1..ddb73f7 100644
--- a/libsoup/soup.h
+++ b/libsoup/soup.h
@@ -15,6 +15,7 @@ extern "C" {
 #include <libsoup/soup-auth-domain.h>
 #include <libsoup/soup-auth-domain-basic.h>
 #include <libsoup/soup-auth-domain-digest.h>
+#include <libsoup/soup-content-sniffer.h>
 #include <libsoup/soup-cookie.h>
 #include <libsoup/soup-cookie-jar.h>
 #include <libsoup/soup-cookie-jar-text.h>
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 0d46df5..c76ddbe 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -26,6 +26,7 @@ noinst_PROGRAMS =	\
 	redirect-test	\
 	simple-httpd	\
 	simple-proxy	\
+	sniffing-test   \
 	streaming-test	\
 	timeout-test	\
 	uri-parsing	\
@@ -58,6 +59,7 @@ redirect_test_SOURCES = redirect-test.c $(TEST_SRCS)
 server_auth_test_SOURCES = server-auth-test.c $(TEST_SRCS)
 simple_httpd_SOURCES = simple-httpd.c
 simple_proxy_SOURCES = simple-proxy.c
+sniffing_test_SOURCES = sniffing-test.c  $(TEST_SRCS)
 ssl_test_SOURCES = ssl-test.c $(TEST_SRCS)
 streaming_test_SOURCES = streaming-test.c $(TEST_SRCS)
 timeout_test_SOURCES = timeout-test.c $(TEST_SRCS)
diff --git a/tests/resources/mbox b/tests/resources/mbox
new file mode 100644
index 0000000..929ad2b
--- /dev/null
+++ b/tests/resources/mbox
@@ -0,0 +1,16 @@
+From email here Wed Jun 17 21:20:48 2009
+Return-path: <email here>
+Envelope-to: email here
+Delivery-date: Wed, 17 Jun 2009 21:20:48 -0300
+Received: from email by here.domain with local (Exim 4.69)
+	(envelope-from <email here>)
+	id 1MH5N2-0008Lq-7c
+	for email here; Wed, 17 Jun 2009 21:20:48 -0300
+To: email here
+Subject: This is just so that I have a mailbox
+Message-Id: <E1MH5N2-0008Lq-7c here domain>
+From: A Nice User <email here>
+Date: Wed, 17 Jun 2009 21:20:48 -0300
+
+This is a dumb email.
+
diff --git a/tests/sniffing-test.c b/tests/sniffing-test.c
new file mode 100644
index 0000000..7adf202
--- /dev/null
+++ b/tests/sniffing-test.c
@@ -0,0 +1,227 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2009 Gustavo Noronha Silva <gns gnome org>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <libsoup/soup.h>
+
+#include "test-utils.h"
+
+SoupSession *session;
+SoupURI *base_uri;
+SoupMessageBody *chunk_data;
+
+static void
+server_callback (SoupServer *server, SoupMessage *msg,
+		 const char *path, GHashTable *query,
+		 SoupClientContext *context, gpointer data)
+{
+	GError *error = NULL;
+	char *contents;
+	gsize length;
+
+	if (msg->method != SOUP_METHOD_GET) {
+		soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
+		return;
+	}
+
+	soup_message_set_status (msg, SOUP_STATUS_OK);
+
+	if (!strcmp (path, "/mbox")) {
+		g_file_get_contents ("resources/mbox",
+				     &contents, &length,
+				     &error);
+
+		if (error) {
+			g_error ("%s", error->message);
+			g_error_free (error);
+			exit (1);
+		}
+
+		soup_message_set_response (msg, "text/plain",
+					   SOUP_MEMORY_TAKE,
+					   contents,
+					   length);
+	}
+}
+
+static gboolean
+unpause_msg (gpointer data)
+{
+	SoupMessage *msg = (SoupMessage*)data;
+	soup_session_unpause_message (session, msg);
+	return FALSE;
+}
+
+
+static void
+content_sniffed (SoupMessage *msg, char *content_type, gpointer data)
+{
+	gboolean should_pause = GPOINTER_TO_INT (data);
+
+	if (g_object_get_data (G_OBJECT (msg), "got-chunk")) {
+		debug_printf (1, "  got-chunk got emitted before content-sniffed\n");
+		errors++;
+	}
+
+	g_object_set_data (G_OBJECT (msg), "content-sniffed", GINT_TO_POINTER (TRUE));
+
+	if (should_pause) {
+		soup_session_pause_message (session, msg);
+		g_idle_add (unpause_msg, msg);
+	}
+}
+
+static void
+got_headers (SoupMessage *msg, gpointer data)
+{
+	gboolean should_pause = GPOINTER_TO_INT (data);
+
+	if (g_object_get_data (G_OBJECT (msg), "content-sniffed")) {
+		debug_printf (1, "  content-sniffed got emitted before got-headers\n");
+		errors++;
+	}
+
+	g_object_set_data (G_OBJECT (msg), "got-headers", GINT_TO_POINTER (TRUE));
+
+	if (should_pause) {
+		soup_session_pause_message (session, msg);
+		g_idle_add (unpause_msg, msg);
+	}
+}
+
+static void
+got_chunk (SoupMessage *msg, SoupBuffer *chunk, gpointer data)
+{
+	gboolean should_accumulate = GPOINTER_TO_INT (data);
+
+	g_object_set_data (G_OBJECT (msg), "got-chunk", GINT_TO_POINTER (TRUE));
+
+	if (!should_accumulate) {
+		if (!chunk_data)
+			chunk_data = soup_message_body_new ();
+		soup_message_body_append_buffer (chunk_data, chunk);
+	}
+}
+
+static void
+finished (SoupSession *session, SoupMessage *msg, gpointer data)
+{
+	GMainLoop *loop = (GMainLoop*)data;
+	g_main_loop_quit (loop);
+}
+
+static void
+do_signals_test (gboolean should_content_sniff,
+		 gboolean should_pause,
+		 gboolean should_accumulate)
+{
+	SoupURI *uri = soup_uri_new_with_base (base_uri, "/mbox");
+	SoupMessage *msg = soup_message_new_from_uri ("GET", uri);
+	GMainLoop *loop = g_main_loop_new (NULL, TRUE);
+	char *contents;
+	gsize length;
+	GError *error = NULL;
+	SoupBuffer *body;
+
+	soup_message_body_set_accumulate (msg->response_body, should_accumulate);
+
+	g_object_connect (msg,
+			  "signal::got-headers", got_headers, GINT_TO_POINTER (should_pause),
+			  "signal::got-chunk", got_chunk, GINT_TO_POINTER (should_accumulate),
+			  "signal::content_sniffed", content_sniffed, GINT_TO_POINTER (should_pause),
+			  NULL);
+
+	g_object_ref (msg);
+	soup_session_queue_message (session, msg, finished, loop);
+
+	g_main_loop_run (loop);
+
+	if (!should_content_sniff &&
+	    g_object_get_data (G_OBJECT (msg), "content-sniffed")) {
+		debug_printf (1, "  content-sniffed got emitted without a sniffer\n");
+		errors++;
+	} else if (should_content_sniff &&
+		   !g_object_get_data (G_OBJECT (msg), "content-sniffed")) {
+		debug_printf (1, "  content-sniffed did not get emitted\n");
+		errors++;
+	}
+
+	g_file_get_contents ("resources/mbox",
+			     &contents, &length,
+			     &error);
+
+	if (error) {
+		g_error ("%s", error->message);
+		g_error_free (error);
+		exit (1);
+	}
+
+	if (!should_accumulate) {
+		body = soup_message_body_flatten (chunk_data);
+		soup_message_body_free (chunk_data);
+		chunk_data = NULL;
+	} else
+		body = soup_message_body_flatten (msg->response_body);
+
+	if (body->length != length) {
+		debug_printf (1, "  lengths do not match\n");
+		errors++;
+	}
+
+	if (memcmp (body->data, contents, length)) {
+		debug_printf (1, "  downloaded data does not match\n");
+		errors++;
+	}
+
+	g_free (contents);
+	soup_buffer_free (body);
+
+	soup_uri_free (uri);
+	g_object_unref (msg);
+	g_main_loop_unref (loop);
+}
+
+int
+main (int argc, char **argv)
+{
+	SoupServer *server;
+	SoupContentSniffer *sniffer;
+
+	test_init (argc, argv, NULL);
+
+	server = soup_test_server_new (TRUE);
+	soup_server_add_handler (server, NULL, server_callback, NULL, NULL);
+	base_uri = soup_uri_new ("http://127.0.0.1/";);
+	soup_uri_set_port (base_uri, soup_server_get_port (server));
+
+	session = soup_session_async_new ();
+
+	/* No sniffer, no content_sniffed should be emitted */
+	do_signals_test (FALSE, FALSE, FALSE);
+	do_signals_test (FALSE, FALSE, TRUE);
+
+	do_signals_test (FALSE, TRUE, TRUE);
+	do_signals_test (FALSE, TRUE, FALSE);
+
+	sniffer = soup_content_sniffer_new ();
+	soup_session_add_feature (session, (SoupSessionFeature*)sniffer);
+
+	/* Now, with a sniffer, content_sniffed must be emitted after
+	 * got-headers, and before got-chunk.
+	 */
+	do_signals_test (TRUE, FALSE, FALSE);
+	do_signals_test (TRUE, FALSE, TRUE);
+
+	do_signals_test (TRUE, TRUE, TRUE);
+	do_signals_test (TRUE, TRUE, FALSE);
+
+	soup_uri_free (base_uri);
+
+	test_cleanup ();
+	return errors != 0;
+}



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]