[gmime] Added support for inline-PGP



commit 16dbb8152318889bba3af91b8b96cb71bd45e7cc
Author: Jeffrey Stedfast <jestedfa microsoft com>
Date:   Tue Mar 14 10:33:13 2017 -0400

    Added support for inline-PGP

 gmime/gmime-crypto-context.c |   11 +-
 gmime/gmime-error.h          |   11 +-
 gmime/gmime-part.c           |  289 ++++++++++++++++++++++++++++++++++++++----
 gmime/gmime-part.h           |   13 ++-
 tests/test-pgpmime.c         |  164 ++++++++++++++++++++++++
 5 files changed, 448 insertions(+), 40 deletions(-)
---
diff --git a/gmime/gmime-crypto-context.c b/gmime/gmime-crypto-context.c
index a428b29..1d6a11f 100644
--- a/gmime/gmime-crypto-context.c
+++ b/gmime/gmime-crypto-context.c
@@ -439,10 +439,9 @@ crypto_encrypt (GMimeCryptoContext *ctx, gboolean sign, const char *userid, GMim
  * g_mime_crypto_context_encrypt:
  * @ctx: a #GMimeCryptoContext
  * @sign: sign as well as encrypt
- * @userid: key id (or email address) to use when signing (assuming @sign is %TRUE)
- * @flags: a #GMimeEncryptFlags
- * @recipients: (element-type utf8): an array of recipient key ids
- *   and/or email addresses
+ * @userid: the key id (or email address) to use when signing (assuming @sign is %TRUE)
+ * @flags: a set of #GMimeEncryptFlags
+ * @recipients: (element-type utf8): an array of recipient key ids and/or email addresses
  * @istream: cleartext input stream
  * @ostream: ciphertext output stream
  * @err: a #GError
@@ -478,8 +477,8 @@ crypto_decrypt (GMimeCryptoContext *ctx, GMimeDecryptFlags flags, const char *se
 /**
  * g_mime_crypto_context_decrypt:
  * @ctx: a #GMimeCryptoContext
- * @flags: a #GMimeDecryptFlags
- * @session_key: session key to use or %NULL
+ * @flags: a set of #GMimeDecryptFlags
+ * @session_key: the session key to use or %NULL
  * @istream: input/ciphertext stream
  * @ostream: output/cleartext stream
  * @err: a #GError
diff --git a/gmime/gmime-error.h b/gmime/gmime-error.h
index 8bd63ae..7511057 100644
--- a/gmime/gmime-error.h
+++ b/gmime/gmime-error.h
@@ -48,12 +48,11 @@ extern GQuark gmime_error_quark;
 
 /* errno is a positive value, so negative values should be safe to use */
 enum {
-       GMIME_ERROR_GENERAL             =  0,
-       GMIME_ERROR_NOT_SUPPORTED       = -1,
-       GMIME_ERROR_PARSE_ERROR         = -2,
-       GMIME_ERROR_PROTOCOL_ERROR      = -3,
-       GMIME_ERROR_BAD_PASSWORD        = -4,
-       GMIME_ERROR_NO_VALID_RECIPIENTS = -5
+       GMIME_ERROR_GENERAL             = -1,
+       GMIME_ERROR_NOT_SUPPORTED       = -2,
+       GMIME_ERROR_INVALID_OPERATION   = -3,
+       GMIME_ERROR_PARSE_ERROR         = -4,
+       GMIME_ERROR_PROTOCOL_ERROR      = -5
 };
 
 
diff --git a/gmime/gmime-part.c b/gmime/gmime-part.c
index ea06c6d..211ea27 100644
--- a/gmime/gmime-part.c
+++ b/gmime/gmime-part.c
@@ -28,6 +28,7 @@
 #include <string.h>
 
 #include "gmime-part.h"
+#include "gmime-error.h"
 #include "gmime-utils.h"
 #include "gmime-common.h"
 #include "gmime-internal.h"
@@ -40,6 +41,7 @@
 #include "gmime-filter-md5.h"
 #include "gmime-table-private.h"
 
+#define _(x) x
 #define d(x)
 
 
@@ -916,6 +918,55 @@ g_mime_part_get_filename (GMimePart *mime_part)
 }
 
 
+static void
+set_content (GMimePart *mime_part, GMimeDataWrapper *content)
+{
+       if (mime_part->content)
+               g_object_unref (mime_part->content);
+       
+       mime_part->content = content;
+       g_object_ref (content);
+}
+
+
+/**
+ * g_mime_part_set_content:
+ * @mime_part: a #GMimePart object
+ * @content: a #GMimeDataWrapper content object
+ *
+ * Sets the content on the mime part.
+ **/
+void
+g_mime_part_set_content (GMimePart *mime_part, GMimeDataWrapper *content)
+{
+       g_return_if_fail (GMIME_IS_PART (mime_part));
+       
+       if (mime_part->content == content)
+               return;
+       
+       GMIME_PART_GET_CLASS (mime_part)->set_content (mime_part, content);
+}
+
+
+/**
+ * g_mime_part_get_content:
+ * @mime_part: a #GMimePart object
+ *
+ * Gets the internal data-wrapper of the specified mime part, or %NULL
+ * on error.
+ *
+ * Returns: (transfer none): the data-wrapper for the mime part's
+ * contents.
+ **/
+GMimeDataWrapper *
+g_mime_part_get_content (GMimePart *mime_part)
+{
+       g_return_val_if_fail (GMIME_IS_PART (mime_part), NULL);
+       
+       return mime_part->content;
+}
+
+
 /**
  * g_mime_part_set_openpgp_data:
  * @mime_part: a #GMimePart
@@ -951,50 +1002,236 @@ g_mime_part_get_openpgp_data (GMimePart *mime_part)
 }
 
 
-static void
-set_content (GMimePart *mime_part, GMimeDataWrapper *content)
+/**
+ * g_mime_part_openpgp_encrypt:
+ * @mime_part: a #GMimePart
+ * @sign: %TRUE if the content should also be signed; otherwise, %FALSE
+ * @userid: the key id (or email address) to use when signing (assuming @sign is %TRUE)
+ * @flags: a set of #GMimeEncryptFlags
+ * @recipients: (element-type utf8): an array of recipient key ids and/or email addresses
+ * @err: a #GError
+ *
+ * Encrypts (and optionally signs) the content of the @mime_part and then replaces
+ * the content with the new, encrypted, content.
+ *
+ * Returns: %TRUE on success or %FALSE on error.
+ **/
+gboolean
+g_mime_part_openpgp_encrypt (GMimePart *mime_part, gboolean sign, const char *userid,
+                            GMimeEncryptFlags flags, GPtrArray *recipients, GError **err)
 {
-       if (mime_part->content)
-               g_object_unref (mime_part->content);
+       GMimeStream *istream, *encrypted;
+       GMimeCryptoContext *ctx;
+       int rv;
        
-       mime_part->content = content;
-       g_object_ref (content);
+       g_return_val_if_fail (GMIME_IS_PART (mime_part), FALSE);
+       
+       if (mime_part->content == NULL) {
+               g_set_error_literal (err, GMIME_ERROR, GMIME_ERROR_INVALID_OPERATION,
+                                    _("No content set on the MIME part."));
+               return FALSE;
+       }
+       
+       if (!(ctx = g_mime_crypto_context_new ("application/pgp-encrypted"))) {
+               g_set_error_literal (err, GMIME_ERROR, GMIME_ERROR_NOT_SUPPORTED,
+                                    _("No crypto context registered for application/pgp-encrypted."));
+               return FALSE;
+       }
+       
+       encrypted = g_mime_stream_mem_new ();
+       istream = g_mime_stream_mem_new ();
+       g_mime_data_wrapper_write_to_stream (mime_part->content, istream);
+       g_mime_stream_reset (istream);
+       
+       rv = g_mime_crypto_context_encrypt (ctx, sign, userid, flags, recipients, istream, encrypted, err);
+       g_object_unref (istream);
+       g_object_unref (ctx);
+       
+       if (rv == -1) {
+               g_object_unref (encrypted);
+               return FALSE;
+       }
+       
+       g_mime_stream_reset (encrypted);
+       
+       g_mime_data_wrapper_set_encoding (mime_part->content, GMIME_CONTENT_ENCODING_DEFAULT);
+       g_mime_data_wrapper_set_stream (mime_part->content, encrypted);
+       mime_part->encoding = GMIME_CONTENT_ENCODING_7BIT;
+       mime_part->openpgp = GMIME_OPENPGP_DATA_ENCRYPTED;
+       g_object_unref (encrypted);
+       
+       return TRUE;
 }
 
 
 /**
- * g_mime_part_set_content:
- * @mime_part: a #GMimePart object
- * @content: a #GMimeDataWrapper content object
+ * g_mime_part_openpgp_decrypt:
+ * @mime_part: a #GMimePart
+ * @flags: a set of #GMimeDecryptFlags
+ * @session_key: the session key to use or %NULL
+ * @err: a #GError
  *
- * Sets the content on the mime part.
+ * Decrypts the content of the @mime_part and then replaces the content with
+ * the new, decrypted, content.
+ *
+ * Returns: (transfer full): a #GMimeDecryptResult on success or %NULL on error.
  **/
-void
-g_mime_part_set_content (GMimePart *mime_part, GMimeDataWrapper *content)
+GMimeDecryptResult *
+g_mime_part_openpgp_decrypt (GMimePart *mime_part, GMimeDecryptFlags flags, const char *session_key, GError 
**err)
 {
-       g_return_if_fail (GMIME_IS_PART (mime_part));
+       GMimeStream *istream, *decrypted;
+       GMimeDecryptResult *result;
+       GMimeCryptoContext *ctx;
        
-       if (mime_part->content == content)
-               return;
+       g_return_val_if_fail (GMIME_IS_PART (mime_part), FALSE);
        
-       GMIME_PART_GET_CLASS (mime_part)->set_content (mime_part, content);
+       if (mime_part->content == NULL) {
+               g_set_error_literal (err, GMIME_ERROR, GMIME_ERROR_INVALID_OPERATION,
+                                    _("No content set on the MIME part."));
+               return NULL;
+       }
+       
+       if (!(ctx = g_mime_crypto_context_new ("application/pgp-encrypted"))) {
+               g_set_error_literal (err, GMIME_ERROR, GMIME_ERROR_NOT_SUPPORTED,
+                                    _("No crypto context registered for application/pgp-encrypted."));
+               return NULL;
+       }
+       
+       decrypted = g_mime_stream_mem_new ();
+       istream = g_mime_stream_mem_new ();
+       g_mime_data_wrapper_write_to_stream (mime_part->content, istream);
+       g_mime_stream_reset (istream);
+
+       result = g_mime_crypto_context_decrypt (ctx, flags, session_key, istream, decrypted, err);
+       g_object_unref (istream);
+       g_object_unref (ctx);
+       
+       if (result == NULL) {
+               g_object_unref (decrypted);
+               return NULL;
+       }
+       
+       g_mime_stream_reset (decrypted);
+       
+       g_mime_data_wrapper_set_encoding (mime_part->content, GMIME_CONTENT_ENCODING_DEFAULT);
+       g_mime_data_wrapper_set_stream (mime_part->content, decrypted);
+       mime_part->openpgp = GMIME_OPENPGP_DATA_NONE;
+       g_object_unref (decrypted);
+       
+       return result;
 }
 
 
 /**
- * g_mime_part_get_content:
- * @mime_part: a #GMimePart object
+ * g_mime_part_openpgp_sign:
+ * @mime_part: a #GMimePart
+ * @userid: the key id (or email address) to use for signing
+ * @err: a #GError
  *
- * Gets the internal data-wrapper of the specified mime part, or %NULL
- * on error.
+ * Signs the content of the @mime_part and then replaces the content with
+ * the new, signed, content.
  *
- * Returns: (transfer none): the data-wrapper for the mime part's
- * contents.
+ * Returns: %TRUE on success or %FALSE on error.
  **/
-GMimeDataWrapper *
-g_mime_part_get_content (GMimePart *mime_part)
+gboolean
+g_mime_part_openpgp_sign (GMimePart *mime_part, const char *userid, GError **err)
 {
-       g_return_val_if_fail (GMIME_IS_PART (mime_part), NULL);
+       GMimeStream *istream, *ostream;
+       GMimeCryptoContext *ctx;
+       int rv;
        
-       return mime_part->content;
+       g_return_val_if_fail (GMIME_IS_PART (mime_part), FALSE);
+       
+       if (mime_part->content == NULL) {
+               g_set_error_literal (err, GMIME_ERROR, GMIME_ERROR_INVALID_OPERATION,
+                                    _("No content set on the MIME part."));
+               return FALSE;
+       }
+       
+       if (!(ctx = g_mime_crypto_context_new ("application/pgp-signature"))) {
+               g_set_error_literal (err, GMIME_ERROR, GMIME_ERROR_NOT_SUPPORTED,
+                                    _("No crypto context registered for application/pgp-signature."));
+               return FALSE;
+       }
+       
+       ostream = g_mime_stream_mem_new ();
+       istream = g_mime_stream_mem_new ();
+       g_mime_data_wrapper_write_to_stream (mime_part->content, istream);
+       g_mime_stream_reset (istream);
+       
+       rv = g_mime_crypto_context_sign (ctx, FALSE, userid, istream, ostream, err);
+       g_object_unref (istream);
+       g_object_unref (ctx);
+       
+       if (rv == -1) {
+               g_object_unref (ostream);
+               return FALSE;
+       }
+       
+       g_mime_stream_reset (ostream);
+       
+       g_mime_data_wrapper_set_encoding (mime_part->content, GMIME_CONTENT_ENCODING_DEFAULT);
+       g_mime_data_wrapper_set_stream (mime_part->content, ostream);
+       mime_part->encoding = GMIME_CONTENT_ENCODING_7BIT;
+       mime_part->openpgp = GMIME_OPENPGP_DATA_SIGNED;
+       g_object_unref (ostream);
+       
+       return TRUE;
+}
+
+
+/**
+ * g_mime_part_openpgp_verify:
+ * @mime_part: a #GMimePart
+ * @flags: a set of #GMimeVerifyFlags
+ * @err: a #GError
+ *
+ * Verifies the OpenPGP signature of the @mime_part and then replaces the content
+ * with the original, raw, content.
+ *
+ * Returns: (transfer full): a #GMimeSignatureList on success or %NULL on error.
+ **/
+GMimeSignatureList *
+g_mime_part_openpgp_verify (GMimePart *mime_part, GMimeVerifyFlags flags, GError **err)
+{
+       GMimeStream *istream, *extracted;
+       GMimeSignatureList *signatures;
+       GMimeCryptoContext *ctx;
+       
+       g_return_val_if_fail (GMIME_IS_PART (mime_part), FALSE);
+       
+       if (mime_part->content == NULL) {
+               g_set_error_literal (err, GMIME_ERROR, GMIME_ERROR_INVALID_OPERATION,
+                                    _("No content set on the MIME part."));
+               return NULL;
+       }
+       
+       if (!(ctx = g_mime_crypto_context_new ("application/pgp-signature"))) {
+               g_set_error_literal (err, GMIME_ERROR, GMIME_ERROR_NOT_SUPPORTED,
+                                    _("No crypto context registered for application/pgp-signature."));
+               return NULL;
+       }
+       
+       extracted = g_mime_stream_mem_new ();
+       istream = g_mime_stream_mem_new ();
+       g_mime_data_wrapper_write_to_stream (mime_part->content, istream);
+       g_mime_stream_reset (istream);
+
+       signatures = g_mime_crypto_context_verify (ctx, flags, istream, NULL, extracted, err);
+       g_object_unref (istream);
+       g_object_unref (ctx);
+       
+       if (signatures == NULL) {
+               g_object_unref (extracted);
+               return NULL;
+       }
+       
+       g_mime_stream_reset (extracted);
+       
+       g_mime_data_wrapper_set_encoding (mime_part->content, GMIME_CONTENT_ENCODING_DEFAULT);
+       g_mime_data_wrapper_set_stream (mime_part->content, extracted);
+       mime_part->openpgp = GMIME_OPENPGP_DATA_NONE;
+       g_object_unref (extracted);
+       
+       return signatures;
 }
diff --git a/gmime/gmime-part.h b/gmime/gmime-part.h
index 38a9098..d2f720d 100644
--- a/gmime/gmime-part.h
+++ b/gmime/gmime-part.h
@@ -29,6 +29,7 @@
 #include <gmime/gmime-encodings.h>
 #include <gmime/gmime-filter-best.h>
 #include <gmime/gmime-data-wrapper.h>
+#include <gmime/gmime-crypto-context.h>
 
 G_BEGIN_DECLS
 
@@ -117,11 +118,19 @@ gboolean g_mime_part_is_attachment (GMimePart *mime_part);
 void g_mime_part_set_filename (GMimePart *mime_part, const char *filename);
 const char *g_mime_part_get_filename (GMimePart *mime_part);
 
+void g_mime_part_set_content (GMimePart *mime_part, GMimeDataWrapper *content);
+GMimeDataWrapper *g_mime_part_get_content (GMimePart *mime_part);
+
 void g_mime_part_set_openpgp_data (GMimePart *mime_part, GMimeOpenPGPData data);
 GMimeOpenPGPData g_mime_part_get_openpgp_data (GMimePart *mime_part);
 
-void g_mime_part_set_content (GMimePart *mime_part, GMimeDataWrapper *content);
-GMimeDataWrapper *g_mime_part_get_content (GMimePart *mime_part);
+gboolean g_mime_part_openpgp_encrypt (GMimePart *mime_part, gboolean sign, const char *userid,
+                                     GMimeEncryptFlags flags, GPtrArray *recipients, GError **err);
+GMimeDecryptResult *g_mime_part_openpgp_decrypt (GMimePart *mime_part, GMimeDecryptFlags flags,
+                                                const char *session_key, GError **err);
+
+gboolean g_mime_part_openpgp_sign (GMimePart *mime_part, const char *userid, GError **err);
+GMimeSignatureList *g_mime_part_openpgp_verify (GMimePart *mime_part, GMimeVerifyFlags flags, GError **err);
 
 G_END_DECLS
 
diff --git a/tests/test-pgpmime.c b/tests/test-pgpmime.c
index 56f67a8..ce573fd 100644
--- a/tests/test-pgpmime.c
+++ b/tests/test-pgpmime.c
@@ -448,6 +448,146 @@ import_key (GMimeCryptoContext *ctx, const char *path)
        }
 }
 
+static GMimePart *
+create_mime_part (void)
+{
+       GMimeTextPart *part;
+       
+       part = g_mime_text_part_new_with_subtype ("plain");
+       g_mime_text_part_set_text (part, "This is the body of the message...\n\n"
+                                  "Does inline-PGP support work properly?\n\n"
+                                  "Let's find out!\n\n");
+       g_mime_part_set_content_encoding ((GMimePart *) part, GMIME_CONTENT_ENCODING_QUOTEDPRINTABLE);
+       g_mime_text_part_set_charset (part, "UTF-8");
+       
+       return (GMimePart *) part;
+}
+
+static void
+test_openpgp_sign (void)
+{
+       GMimeSignatureList *signatures;
+       GMimeStream *original;
+       GMimePart *mime_part;
+       Exception *ex = NULL;
+       GError *err = NULL;
+       GByteArray *buf[2];
+       
+       mime_part = create_mime_part ();
+       original = mime_part->content->stream;
+       g_object_ref (original);
+       
+       if (!g_mime_part_openpgp_sign (mime_part, "no.user@no.domain", &err)) {
+               ex = exception_new ("signing failed: %s", err->message);
+               g_object_unref (mime_part);
+               g_object_unref (original);
+               g_error_free (err);
+               throw (ex);
+       }
+       
+       if (g_mime_part_get_openpgp_data (mime_part) != GMIME_OPENPGP_DATA_SIGNED) {
+               g_object_unref (mime_part);
+               g_object_unref (original);
+               
+               throw (exception_new ("OpenPGP data property not updated after signing"));
+       }
+       
+       if (!(signatures = g_mime_part_openpgp_verify (mime_part, 0, &err))) {
+               ex = exception_new ("verifying failed: %s", err->message);
+               g_object_unref (mime_part);
+               g_object_unref (original);
+               g_error_free (err);
+               throw (ex);
+       }
+       
+       if (g_mime_part_get_openpgp_data (mime_part) != GMIME_OPENPGP_DATA_NONE) {
+               g_object_unref (signatures);
+               g_object_unref (mime_part);
+               g_object_unref (original);
+               
+               throw (exception_new ("OpenPGP data property not updated after verifying"));
+       }
+
+       buf[0] = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (original));
+       buf[1] = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (mime_part->content->stream));
+       
+       if (buf[0]->len != buf[1]->len || memcmp (buf[0]->data, buf[1]->data, buf[0]->len) != 0)
+               ex = exception_new ("extracted data does not match original cleartext");
+       
+       g_object_unref (signatures);
+       g_object_unref (mime_part);
+       g_object_unref (original);
+       
+       if (ex != NULL)
+               throw (ex);
+}
+
+static void
+test_openpgp_encrypt (gboolean sign)
+{
+       GMimeDecryptResult *result;
+       GMimeStream *original;
+       GMimePart *mime_part;
+       Exception *ex = NULL;
+       GError *err = NULL;
+       GByteArray *buf[2];
+       GPtrArray *rcpts;
+       
+       rcpts = g_ptr_array_new ();
+       g_ptr_array_add (rcpts, "no.user@no.domain");
+       
+       mime_part = create_mime_part ();
+       original = mime_part->content->stream;
+       g_object_ref (original);
+       
+       if (!g_mime_part_openpgp_encrypt (mime_part, sign, "no.user@no.domain", 
GMIME_ENCRYPT_FLAGS_ALWAYS_TRUST, rcpts, &err)) {
+               ex = exception_new ("encrypting failed: %s", err->message);
+               g_ptr_array_free (rcpts, TRUE);
+               g_object_unref (mime_part);
+               g_object_unref (original);
+               g_error_free (err);
+               throw (ex);
+       }
+       
+       g_ptr_array_free (rcpts, TRUE);
+       
+       if (g_mime_part_get_openpgp_data (mime_part) != GMIME_OPENPGP_DATA_ENCRYPTED) {
+               g_object_unref (mime_part);
+               g_object_unref (original);
+               
+               throw (exception_new ("OpenPGP data property not updated after encrypting"));
+       }
+       
+       if (!(result = g_mime_part_openpgp_decrypt (mime_part, 0, NULL, &err))) {
+               ex = exception_new ("decrypting failed: %s", err->message);
+               g_object_unref (mime_part);
+               g_object_unref (original);
+               g_error_free (err);
+               throw (ex);
+       }
+       
+       if (g_mime_part_get_openpgp_data (mime_part) != GMIME_OPENPGP_DATA_NONE) {
+               g_object_unref (mime_part);
+               g_object_unref (original);
+               g_object_unref (result);
+               
+               throw (exception_new ("OpenPGP data property not updated after decrypting"));
+       }
+       
+       buf[0] = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (original));
+       buf[1] = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (mime_part->content->stream));
+       
+       if (buf[0]->len != buf[1]->len || memcmp (buf[0]->data, buf[1]->data, buf[0]->len) != 0)
+               ex = exception_new ("decrypted data does not match original cleartext");
+       
+       g_object_unref (mime_part);
+       g_object_unref (original);
+       g_object_unref (result);
+       
+       if (ex != NULL)
+               throw (ex);
+}
+
 int main (int argc, char *argv[])
 {
 #ifdef ENABLE_CRYPTO
@@ -560,6 +700,30 @@ int main (int argc, char *argv[])
        }
        
        g_free (session_key);
+       
+       testsuite_check ("rfc2440 sign");
+       try {
+               test_openpgp_sign ();
+               testsuite_check_passed ();
+       } catch (ex) {
+               testsuite_check_failed ("rfc2440 sign failed: %s", ex->message);
+       } finally;
+       
+       testsuite_check ("rfc2440 encrypt");
+       try {
+               test_openpgp_encrypt (FALSE);
+               testsuite_check_passed ();
+       } catch (ex) {
+               testsuite_check_failed ("rfc2440 encrypt failed: %s", ex->message);
+       } finally;
+       
+       testsuite_check ("rfc2440 sign+encrypt");
+       try {
+               test_openpgp_encrypt (TRUE);
+               testsuite_check_passed ();
+       } catch (ex) {
+               testsuite_check_failed ("rfc2440 sign+encrypt failed: %s", ex->message);
+       } finally;
 #endif /* GPGME_VERSION NUMBER >= 0x010700 */
        
        g_object_unref (ctx);


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