467 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Diff
		
	
	
	
	
	
			
		
		
	
	
			467 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Diff
		
	
	
	
	
	
| From dfe7218f07ffa70b73c51c71b0f051be926b6d92 Mon Sep 17 00:00:00 2001
 | |
| From: Aleš Matěj <amatej@redhat.com>
 | |
| Date: Tue, 14 May 2019 16:48:13 +0200
 | |
| Subject: [PATCH] Correct pkg count in headers if there were invalid pkgs (RhBug:1596211)
 | |
| 
 | |
| ---
 | |
|  src/createrepo_c.c  | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
 | |
|  src/dumper_thread.c |   4 +++-
 | |
|  src/dumper_thread.h |   3 ++-
 | |
|  src/threads.c       |  23 +++++++++++++++++++++++
 | |
|  src/threads.h       |   5 +++++
 | |
|  src/xml_file.c      | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | |
|  src/xml_file.h      |  15 +++++++++++++++
 | |
|  7 files changed, 260 insertions(+), 16 deletions(-)
 | |
| 
 | |
| diff --git a/src/createrepo_c.c b/src/createrepo_c.c
 | |
| index e16ae34..67c2752 100644
 | |
| --- a/src/createrepo_c.c
 | |
| +++ b/src/createrepo_c.c
 | |
| @@ -124,7 +124,7 @@ fill_pool(GThreadPool *pool,
 | |
|            struct CmdOptions *cmd_options,
 | |
|            GSList **current_pkglist,
 | |
|            FILE *output_pkg_list,
 | |
| -          long *package_count,
 | |
| +          long *task_count,
 | |
|            int  media_id)
 | |
|  {
 | |
|      GQueue queue = G_QUEUE_INIT;
 | |
| @@ -259,13 +259,13 @@ fill_pool(GThreadPool *pool,
 | |
|  
 | |
|      // Push sorted tasks into the thread pool
 | |
|      while ((task = g_queue_pop_head(&queue)) != NULL) {
 | |
| -        task->id = *package_count;
 | |
| +        task->id = *task_count;
 | |
|          task->media_id = media_id;
 | |
|          g_thread_pool_push(pool, task, NULL);
 | |
| -        ++*package_count;
 | |
| +        ++*task_count;
 | |
|      }
 | |
|  
 | |
| -    return *package_count;
 | |
| +    return *task_count;
 | |
|  }
 | |
|  
 | |
|  
 | |
| @@ -321,6 +321,27 @@ prepare_cache_dir(struct CmdOptions *cmd_options,
 | |
|      return TRUE;
 | |
|  }
 | |
|  
 | |
| +/** Check if task finished without error, if yes
 | |
| + *  use content stats of the new file
 | |
| + *
 | |
| + * @param task          Rewrite pkg count task
 | |
| + * @param filename      Name of file with wrong package count
 | |
| + * @param exit_val      If errors occured set createrepo_c exit value
 | |
| + * @param content_stat  Content stats for filename
 | |
| + *
 | |
| + */
 | |
| +static void
 | |
| +error_check_and_set_content_stat(cr_CompressionTask *task, char *filename, int *exit_val, cr_ContentStat **content_stat){
 | |
| +    if (task->err) {
 | |
| +        g_critical("Cannot rewrite pkg count in %s: %s",
 | |
| +                   filename, task->err->message);
 | |
| +        *exit_val = 2;
 | |
| +    }else{
 | |
| +        cr_contentstat_free(*content_stat, NULL);
 | |
| +        *content_stat = task->stat;
 | |
| +        task->stat = NULL;
 | |
| +    }
 | |
| +}
 | |
|  
 | |
|  int
 | |
|  main(int argc, char **argv)
 | |
| @@ -478,7 +499,7 @@ main(int argc, char **argv)
 | |
|                                            NULL);
 | |
|      g_debug("Thread pool ready");
 | |
|  
 | |
| -    long package_count = 0;
 | |
| +    long task_count = 0;
 | |
|      GSList *current_pkglist = NULL;
 | |
|      /* ^^^ List with basenames of files which will be processed */
 | |
|  
 | |
| @@ -490,26 +511,26 @@ main(int argc, char **argv)
 | |
|                    cmd_options,
 | |
|                    ¤t_pkglist,
 | |
|                    output_pkg_list,
 | |
| -                  &package_count,
 | |
| +                  &task_count,
 | |
|                    media_id);
 | |
|          g_free(tmp_in_dir);
 | |
|      }
 | |
|  
 | |
| -    g_debug("Package count: %ld", package_count);
 | |
| -    g_message("Directory walk done - %ld packages", package_count);
 | |
| +    g_debug("Package count: %ld", task_count);
 | |
| +    g_message("Directory walk done - %ld packages", task_count);
 | |
|  
 | |
|      if (output_pkg_list)
 | |
|          fclose(output_pkg_list);
 | |
|  
 | |
|  
 | |
|      // Load old metadata if --update
 | |
|      cr_Metadata *old_metadata = NULL;
 | |
|      struct cr_MetadataLocation *old_metadata_location = NULL;
 | |
|  
 | |
| -    if (!package_count)
 | |
| +    if (!task_count)
 | |
|          g_debug("No packages found - skipping metadata loading");
 | |
|  
 | |
| -    if (package_count && cmd_options->update) {
 | |
| +    if (task_count && cmd_options->update) {
 | |
|          int ret;
 | |
|          old_metadata = cr_metadata_new(CR_HT_KEY_FILENAME, 1, current_pkglist);
 | |
|          cr_metadata_set_dupaction(old_metadata, CR_HT_DUPACT_REMOVEALL);
 | |
| @@ -741,9 +762,9 @@ main(int argc, char **argv)
 | |
|  
 | |
|      // Set number of packages
 | |
|      g_debug("Setting number of packages");
 | |
| -    cr_xmlfile_set_num_of_pkgs(pri_cr_file, package_count, NULL);
 | |
| -    cr_xmlfile_set_num_of_pkgs(fil_cr_file, package_count, NULL);
 | |
| -    cr_xmlfile_set_num_of_pkgs(oth_cr_file, package_count, NULL);
 | |
| +    cr_xmlfile_set_num_of_pkgs(pri_cr_file, task_count, NULL);
 | |
| +    cr_xmlfile_set_num_of_pkgs(fil_cr_file, task_count, NULL);
 | |
| +    cr_xmlfile_set_num_of_pkgs(oth_cr_file, task_count, NULL);
 | |
|  
 | |
|      // Open sqlite databases
 | |
|      gchar *pri_db_filename = NULL;
 | |
| @@ -832,7 +853,8 @@ main(int argc, char **argv)
 | |
|      user_data.checksum_cachedir = cmd_options->checksum_cachedir;
 | |
|      user_data.skip_symlinks     = cmd_options->skip_symlinks;
 | |
|      user_data.repodir_name_len  = strlen(in_dir);
 | |
| -    user_data.package_count     = package_count;
 | |
| +    user_data.task_count        = task_count;
 | |
| +    user_data.package_count     = 0;
 | |
|      user_data.skip_stat         = cmd_options->skip_stat;
 | |
|      user_data.old_metadata      = old_metadata;
 | |
|      user_data.mutex_pri         = g_mutex_new();
 | |
| @@ -876,6 +898,59 @@ main(int argc, char **argv)
 | |
|      cr_xmlfile_close(fil_cr_file, NULL);
 | |
|      cr_xmlfile_close(oth_cr_file, NULL);
 | |
|  
 | |
| +
 | |
| +    /* At the time of writing xml metadata headers we haven't yet parsed all
 | |
| +     * the packages and we don't know whether there were some invalid ones,
 | |
| +     * therefore we write the task count into the headers instead of the actual package count.
 | |
| +     * If there actually were some invalid packages we have to correct this value
 | |
| +     * that unfortunately means we have to decompress metadata files change package
 | |
| +     * count value and compress them again.
 | |
| +     */
 | |
| +    if (user_data.package_count != user_data.task_count){
 | |
| +        g_message("Warning: There were some invalid packages: we have to recompress other, filelists and primary xml metadata files in order to have correct package counts");
 | |
| +
 | |
| +        GThreadPool *rewrite_pkg_count_pool = g_thread_pool_new(cr_rewrite_pkg_count_thread,
 | |
| +                                                                &user_data, 3, FALSE, NULL);
 | |
| +
 | |
| +        cr_CompressionTask *pri_rewrite_pkg_count_task;
 | |
| +        cr_CompressionTask *fil_rewrite_pkg_count_task;
 | |
| +        cr_CompressionTask *oth_rewrite_pkg_count_task;
 | |
| +
 | |
| +        pri_rewrite_pkg_count_task = cr_compressiontask_new(pri_xml_filename,
 | |
| +                                                            NULL,
 | |
| +                                                            xml_compression,
 | |
| +                                                            cmd_options->repomd_checksum_type,
 | |
| +                                                            1,
 | |
| +                                                            &tmp_err);
 | |
| +        g_thread_pool_push(rewrite_pkg_count_pool, pri_rewrite_pkg_count_task, NULL);
 | |
| +
 | |
| +        fil_rewrite_pkg_count_task = cr_compressiontask_new(fil_xml_filename,
 | |
| +                                                            NULL,
 | |
| +                                                            xml_compression,
 | |
| +                                                            cmd_options->repomd_checksum_type,
 | |
| +                                                            1,
 | |
| +                                                            &tmp_err);
 | |
| +        g_thread_pool_push(rewrite_pkg_count_pool, fil_rewrite_pkg_count_task, NULL);
 | |
| +
 | |
| +        oth_rewrite_pkg_count_task = cr_compressiontask_new(oth_xml_filename,
 | |
| +                                                            NULL,
 | |
| +                                                            xml_compression,
 | |
| +                                                            cmd_options->repomd_checksum_type,
 | |
| +                                                            1,
 | |
| +                                                            &tmp_err);
 | |
| +        g_thread_pool_push(rewrite_pkg_count_pool, oth_rewrite_pkg_count_task, NULL);
 | |
| +
 | |
| +        g_thread_pool_free(rewrite_pkg_count_pool, FALSE, TRUE);
 | |
| +
 | |
| +        error_check_and_set_content_stat(pri_rewrite_pkg_count_task, pri_xml_filename, &exit_val, &pri_stat);
 | |
| +        error_check_and_set_content_stat(fil_rewrite_pkg_count_task, fil_xml_filename, &exit_val, &fil_stat);
 | |
| +        error_check_and_set_content_stat(oth_rewrite_pkg_count_task, oth_xml_filename, &exit_val, &oth_stat);
 | |
| +
 | |
| +        cr_compressiontask_free(pri_rewrite_pkg_count_task, NULL);
 | |
| +        cr_compressiontask_free(fil_rewrite_pkg_count_task, NULL);
 | |
| +        cr_compressiontask_free(oth_rewrite_pkg_count_task, NULL);
 | |
| +    }
 | |
| +
 | |
|      g_queue_free(user_data.buffer);
 | |
|      g_mutex_free(user_data.mutex_buffer);
 | |
|      g_cond_free(user_data.cond_pri);
 | |
| diff --git a/src/dumper_thread.c b/src/dumper_thread.c
 | |
| index fbaa5be..e282f96 100644
 | |
| --- a/src/dumper_thread.c
 | |
| +++ b/src/dumper_thread.c
 | |
| @@ -74,6 +74,8 @@ write_pkg(long id,
 | |
|      g_mutex_lock(udata->mutex_pri);
 | |
|      while (udata->id_pri != id)
 | |
|          g_cond_wait (udata->cond_pri, udata->mutex_pri);
 | |
| +
 | |
| +    udata->package_count++;
 | |
|      ++udata->id_pri;
 | |
|      cr_xmlfile_add_chunk(udata->pri_f, (const char *) res.primary, &tmp_err);
 | |
|      if (tmp_err) {
 | |
| @@ -476,7 +478,7 @@ cr_dumper_thread(gpointer data, gpointer user_data)
 | |
|  
 | |
|      if (g_queue_get_length(udata->buffer) < MAX_TASK_BUFFER_LEN
 | |
|          && udata->id_pri != task->id
 | |
| -        && udata->package_count > (task->id + 1))
 | |
| +        && udata->task_count > (task->id + 1))
 | |
|      {
 | |
|          // If:
 | |
|          //  * this isn't our turn
 | |
| diff --git a/src/dumper_thread.h b/src/dumper_thread.h
 | |
| index ed21053..4e18869 100644
 | |
| --- a/src/dumper_thread.h
 | |
| +++ b/src/dumper_thread.h
 | |
| @@ -61,7 +61,8 @@ struct UserData {
 | |
|      cr_ChecksumType checksum_type;  // Constant representing selected checksum
 | |
|      const char *checksum_cachedir;  // Dir with cached checksums
 | |
|      gboolean skip_symlinks;         // Skip symlinks
 | |
| -    long package_count;             // Total number of packages to process
 | |
| +    long task_count;                // Total number of task to process
 | |
| +    long package_count;             // Total number of packages processed
 | |
|  
 | |
|      // Update stuff
 | |
|      gboolean skip_stat;             // Skip stat() while updating
 | |
| diff --git a/src/threads.c b/src/threads.c
 | |
| index aee07d1..844e900 100644
 | |
| --- a/src/threads.c
 | |
| +++ b/src/threads.c
 | |
| @@ -21,6 +21,7 @@
 | |
|  #include "threads.h"
 | |
|  #include "error.h"
 | |
|  #include "misc.h"
 | |
| +#include "dumper_thread.h"
 | |
|  
 | |
|  #define ERR_DOMAIN      CREATEREPO_C_ERROR
 | |
|  
 | |
| @@ -108,6 +109,28 @@ cr_compressing_thread(gpointer data, G_GNUC_UNUSED gpointer user_data)
 | |
|      }
 | |
|  }
 | |
|  
 | |
| +void
 | |
| +cr_rewrite_pkg_count_thread(gpointer data, gpointer user_data)
 | |
| +{
 | |
| +    cr_CompressionTask *task = data;
 | |
| +    struct UserData *ud = user_data;
 | |
| +    GError *tmp_err = NULL;
 | |
| +
 | |
| +    assert(task);
 | |
| +
 | |
| +    cr_rewrite_header_package_count(task->src,
 | |
| +                                    task->type,
 | |
| +                                    ud->package_count,
 | |
| +                                    ud->task_count,
 | |
| +                                    task->stat,
 | |
| +                                    &tmp_err);
 | |
| +
 | |
| +    if (tmp_err) {
 | |
| +        // Error encountered
 | |
| +        g_propagate_error(&task->err, tmp_err);
 | |
| +    }
 | |
| +}
 | |
| +
 | |
|  /** Parallel Repomd Record Fill */
 | |
|  
 | |
|  cr_RepomdRecordFillTask *
 | |
| diff --git a/src/threads.h b/src/threads.h
 | |
| index 2d554cd..19ba917 100644
 | |
| --- a/src/threads.h
 | |
| +++ b/src/threads.h
 | |
| @@ -150,6 +150,11 @@ cr_repomdrecordfilltask_free(cr_RepomdRecordFillTask *task, GError **err);
 | |
|  void
 | |
|  cr_repomd_record_fill_thread(gpointer data, gpointer user_data);
 | |
|  
 | |
| +/** Function for GThread Pool.
 | |
| + */
 | |
| +void
 | |
| +cr_rewrite_pkg_count_thread(gpointer data, gpointer user_data);
 | |
| +
 | |
|  /** @} */
 | |
|  
 | |
|  #ifdef __cplusplus
 | |
| diff --git a/src/xml_file.c b/src/xml_file.c
 | |
| index 65fb945..1d670ae 100644
 | |
| --- a/src/xml_file.c
 | |
| +++ b/src/xml_file.c
 | |
| @@ -18,8 +18,10 @@
 | |
|   */
 | |
|  
 | |
|  #include <glib.h>
 | |
| +#include <glib/gstdio.h>
 | |
|  #include <assert.h>
 | |
|  #include "xml_file.h"
 | |
| +#include <errno.h>
 | |
|  #include "error.h"
 | |
|  #include "xml_dump.h"
 | |
|  #include "compression_wrapper.h"
 | |
| @@ -40,6 +42,9 @@
 | |
|  #define XML_PRESTODELTA_HEADER  XML_HEADER"<prestodelta>\n"
 | |
|  #define XML_UPDATEINFO_HEADER   XML_HEADER"<updates>\n"
 | |
|  
 | |
| +#define XML_MAX_HEADER_SIZE     300
 | |
| +#define XML_RECOMPRESS_BUFFER_SIZE   8192
 | |
| +
 | |
|  #define XML_PRIMARY_FOOTER      "</metadata>"
 | |
|  #define XML_FILELISTS_FOOTER    "</filelists>"
 | |
|  #define XML_OTHER_FOOTER        "</otherdata>"
 | |
| @@ -317,3 +322,121 @@ cr_xmlfile_close(cr_XmlFile *f, GError **err)
 | |
|  
 | |
|      return CRE_OK;
 | |
|  }
 | |
| +
 | |
| +static int
 | |
| +write_modified_header(int task_count,
 | |
| +                      int package_count,
 | |
| +                      cr_XmlFile *cr_file,
 | |
| +                      gchar *header_buf,
 | |
| +                      int header_len,
 | |
| +                      GError **err)
 | |
| +{
 | |
| +    GError *tmp_err = NULL;
 | |
| +    gchar *package_count_string;
 | |
| +    gchar *task_count_string;
 | |
| +    int bytes_written = 0;
 | |
| +    int package_count_string_len = rasprintf(&package_count_string, "packages=\"%i\"", package_count);
 | |
| +    int task_count_string_len = rasprintf(&task_count_string, "packages=\"%i\"", task_count);
 | |
| +
 | |
| +    gchar *pointer_to_pkgs = strstr(header_buf, task_count_string);
 | |
| +    if (!pointer_to_pkgs){
 | |
| +        g_free(package_count_string);
 | |
| +        g_free(task_count_string);
 | |
| +        return 0;
 | |
| +    }
 | |
| +    gchar *pointer_to_pkgs_end = pointer_to_pkgs + task_count_string_len;
 | |
| +
 | |
| +    bytes_written += cr_write(cr_file->f, header_buf, pointer_to_pkgs - header_buf, &tmp_err);
 | |
| +    if (!tmp_err)
 | |
| +        bytes_written += cr_write(cr_file->f, package_count_string, package_count_string_len, &tmp_err);
 | |
| +    if (!tmp_err)
 | |
| +        bytes_written += cr_write(cr_file->f, pointer_to_pkgs_end, header_len - (pointer_to_pkgs_end - header_buf), &tmp_err);
 | |
| +    if (tmp_err) {
 | |
| +        g_propagate_prefixed_error(err, tmp_err, "Error encountered while writing header part:");
 | |
| +        g_free(package_count_string);
 | |
| +        g_free(task_count_string);
 | |
| +        return 0;
 | |
| +    }
 | |
| +    g_free(package_count_string);
 | |
| +    g_free(task_count_string);
 | |
| +    return bytes_written;
 | |
| +}
 | |
| +
 | |
| +void
 | |
| +cr_rewrite_header_package_count(gchar *original_filename,
 | |
| +                                cr_CompressionType xml_compression,
 | |
| +                                int package_count,
 | |
| +                                int task_count,
 | |
| +                                cr_ContentStat *file_stat,
 | |
| +                                GError **err)
 | |
| +{
 | |
| +    GError *tmp_err = NULL;
 | |
| +    CR_FILE *original_file = cr_open(original_filename, CR_CW_MODE_READ, CR_CW_AUTO_DETECT_COMPRESSION, &tmp_err);
 | |
| +    if (tmp_err) {
 | |
| +        g_propagate_prefixed_error(err, tmp_err, "Error encountered while reopening for reading:");
 | |
| +        return;
 | |
| +    }
 | |
| +
 | |
| +    gchar *tmp_xml_filename = g_strconcat(original_filename, ".tmp", NULL);
 | |
| +    cr_XmlFile *new_file = cr_xmlfile_sopen_primary(tmp_xml_filename,
 | |
| +                                                    xml_compression,
 | |
| +                                                    file_stat,
 | |
| +                                                    &tmp_err);
 | |
| +    if (tmp_err) {
 | |
| +        g_propagate_prefixed_error(err, tmp_err, "Error encountered while opening for writing:");
 | |
| +        cr_close(original_file, NULL);
 | |
| +        g_free(tmp_xml_filename);
 | |
| +        return;
 | |
| +    }
 | |
| +
 | |
| +    gchar header_buf[XML_MAX_HEADER_SIZE];
 | |
| +    int len_read = cr_read(original_file, header_buf, XML_MAX_HEADER_SIZE, &tmp_err);
 | |
| +    if (!tmp_err)
 | |
| +        write_modified_header(task_count, package_count, new_file, header_buf, len_read, &tmp_err);
 | |
| +    if (tmp_err) {
 | |
| +        g_propagate_prefixed_error(err, tmp_err, "Error encountered while recompressing:");
 | |
| +        cr_xmlfile_close(new_file, NULL);
 | |
| +        cr_close(original_file, NULL);
 | |
| +        g_free(tmp_xml_filename);
 | |
| +        return;
 | |
| +    }
 | |
| +    //Copy the rest of the file
 | |
| +    gchar copy_buf[XML_RECOMPRESS_BUFFER_SIZE];
 | |
| +    while(len_read)
 | |
| +    {
 | |
| +        len_read = cr_read(original_file, copy_buf, XML_RECOMPRESS_BUFFER_SIZE, &tmp_err);
 | |
| +        if (!tmp_err)
 | |
| +            cr_write(new_file->f, copy_buf, len_read, &tmp_err);
 | |
| +        if (tmp_err) {
 | |
| +            g_propagate_prefixed_error(err, tmp_err, "Error encountered while recompressing:");
 | |
| +            cr_xmlfile_close(new_file, NULL);
 | |
| +            cr_close(original_file, NULL);
 | |
| +            g_free(tmp_xml_filename);
 | |
| +            return;
 | |
| +        }
 | |
| +    }
 | |
| +
 | |
| +    new_file->header = 1;
 | |
| +    new_file->footer = 1;
 | |
| +
 | |
| +    cr_xmlfile_close(new_file, &tmp_err);
 | |
| +    if (tmp_err) {
 | |
| +        g_propagate_prefixed_error(err, tmp_err, "Error encountered while writing:");
 | |
| +        cr_close(original_file, NULL);
 | |
| +        g_free(tmp_xml_filename);
 | |
| +        return;
 | |
| +    }
 | |
| +    cr_close(original_file, &tmp_err);
 | |
| +    if (tmp_err) {
 | |
| +        g_propagate_prefixed_error(err, tmp_err, "Error encountered while writing:");
 | |
| +        g_free(tmp_xml_filename);
 | |
| +        return;
 | |
| +    }
 | |
| +
 | |
| +    if (g_rename(tmp_xml_filename, original_filename) == -1) {
 | |
| +        g_propagate_prefixed_error(err, tmp_err, "Error encountered while renaming:");
 | |
| +        g_free(tmp_xml_filename);
 | |
| +        return;
 | |
| +    }
 | |
| +    g_free(tmp_xml_filename);
 | |
| +}
 | |
| diff --git a/src/xml_file.h b/src/xml_file.h
 | |
| index 96ef5e3..6ac4c97 100644
 | |
| --- a/src/xml_file.h
 | |
| +++ b/src/xml_file.h
 | |
| @@ -221,6 +221,21 @@ int cr_xmlfile_add_chunk(cr_XmlFile *f, const char *chunk, GError **err);
 | |
|   */
 | |
|  int cr_xmlfile_close(cr_XmlFile *f, GError **err);
 | |
|  
 | |
| +/** Rewrite package count field in repodata header in xml file.
 | |
| + * In order to do this we have to decompress and after the change
 | |
| + * compress the whole file again, so entirely new file is created.
 | |
| + * @param original_filename     Current file with wrong value in header
 | |
| + * @param package_count         Actual package count (desired value in header)
 | |
| + * @param task_count            Task count (current value in header)
 | |
| + * @param file_stat             cr_ContentStat for stats of the new file, it will be modified
 | |
| + * @param err                   **GError
 | |
| + */
 | |
| +void cr_rewrite_header_package_count(gchar *original_filename,
 | |
| +                                     cr_CompressionType xml_compression,
 | |
| +                                     int package_count,
 | |
| +                                     int task_count,
 | |
| +                                     cr_ContentStat *file_stat,
 | |
| +                                     GError **err);
 | |
|  
 | |
|  /** @} */
 | |
|  
 | |
| --
 | |
| libgit2 0.27.8
 | |
| 
 |