Fossil SCM
Cleanup of the code used to resolve tag names in contexts where an artifact ID can be entered.
Commit
bf56b2ddf4082e7ba0654574f7cd60f6246c54b3
Parent
e7efca9ee92021d…
2 files changed
+8
-22
+43
-69
+8
-22
| --- src/info.c | ||
| +++ src/info.c | ||
| @@ -1056,38 +1056,24 @@ | ||
| 1056 | 1056 | ** Figure out what the artifact ID is and jump to it. |
| 1057 | 1057 | */ |
| 1058 | 1058 | void info_page(void){ |
| 1059 | 1059 | const char *zName; |
| 1060 | 1060 | Blob uuid; |
| 1061 | - int rid, nName; | |
| 1061 | + int rid; | |
| 1062 | 1062 | |
| 1063 | 1063 | zName = P("name"); |
| 1064 | 1064 | if( zName==0 ) fossil_redirect_home(); |
| 1065 | - nName = strlen(zName); | |
| 1066 | - if( nName<4 || nName>UUID_SIZE || !validate16(zName, nName) ){ | |
| 1067 | - switch( sym_tag_to_uuid(zName, &uuid) ){ | |
| 1068 | - case 1: { | |
| 1069 | - /* got one UUID, use it */ | |
| 1070 | - zName = blob_str(&uuid); | |
| 1071 | - break; | |
| 1072 | - } | |
| 1073 | - case 2: { | |
| 1074 | - /* go somewhere to show the multiple UUIDs */ | |
| 1075 | - return; | |
| 1076 | - break; | |
| 1077 | - } | |
| 1078 | - default: { | |
| 1079 | - fossil_redirect_home(); | |
| 1080 | - break; | |
| 1081 | - } | |
| 1082 | - } | |
| 1083 | - } | |
| 1084 | - if( db_exists("SELECT 1 FROM ticket WHERE tkt_uuid GLOB '%s*'", zName) ){ | |
| 1065 | + blob_set(&uuid, zName); | |
| 1066 | + if( name_to_uuid(&uuid, 1) ){ | |
| 1067 | + fossil_redirect_home(); | |
| 1068 | + } | |
| 1069 | + zName = blob_str(&uuid); | |
| 1070 | + if( db_exists("SELECT 1 FROM ticket WHERE tkt_uuid='%s'", zName) ){ | |
| 1085 | 1071 | tktview_page(); |
| 1086 | 1072 | return; |
| 1087 | 1073 | } |
| 1088 | - rid = db_int(0, "SELECT rid FROM blob WHERE uuid GLOB '%s*'", zName); | |
| 1074 | + rid = db_int(0, "SELECT rid FROM blob WHERE uuid='%s'", zName); | |
| 1089 | 1075 | if( rid==0 ){ |
| 1090 | 1076 | style_header("Broken Link"); |
| 1091 | 1077 | @ <p>No such object: %h(zName)</p> |
| 1092 | 1078 | style_footer(); |
| 1093 | 1079 | return; |
| 1094 | 1080 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -1056,38 +1056,24 @@ | |
| 1056 | ** Figure out what the artifact ID is and jump to it. |
| 1057 | */ |
| 1058 | void info_page(void){ |
| 1059 | const char *zName; |
| 1060 | Blob uuid; |
| 1061 | int rid, nName; |
| 1062 | |
| 1063 | zName = P("name"); |
| 1064 | if( zName==0 ) fossil_redirect_home(); |
| 1065 | nName = strlen(zName); |
| 1066 | if( nName<4 || nName>UUID_SIZE || !validate16(zName, nName) ){ |
| 1067 | switch( sym_tag_to_uuid(zName, &uuid) ){ |
| 1068 | case 1: { |
| 1069 | /* got one UUID, use it */ |
| 1070 | zName = blob_str(&uuid); |
| 1071 | break; |
| 1072 | } |
| 1073 | case 2: { |
| 1074 | /* go somewhere to show the multiple UUIDs */ |
| 1075 | return; |
| 1076 | break; |
| 1077 | } |
| 1078 | default: { |
| 1079 | fossil_redirect_home(); |
| 1080 | break; |
| 1081 | } |
| 1082 | } |
| 1083 | } |
| 1084 | if( db_exists("SELECT 1 FROM ticket WHERE tkt_uuid GLOB '%s*'", zName) ){ |
| 1085 | tktview_page(); |
| 1086 | return; |
| 1087 | } |
| 1088 | rid = db_int(0, "SELECT rid FROM blob WHERE uuid GLOB '%s*'", zName); |
| 1089 | if( rid==0 ){ |
| 1090 | style_header("Broken Link"); |
| 1091 | @ <p>No such object: %h(zName)</p> |
| 1092 | style_footer(); |
| 1093 | return; |
| 1094 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -1056,38 +1056,24 @@ | |
| 1056 | ** Figure out what the artifact ID is and jump to it. |
| 1057 | */ |
| 1058 | void info_page(void){ |
| 1059 | const char *zName; |
| 1060 | Blob uuid; |
| 1061 | int rid; |
| 1062 | |
| 1063 | zName = P("name"); |
| 1064 | if( zName==0 ) fossil_redirect_home(); |
| 1065 | blob_set(&uuid, zName); |
| 1066 | if( name_to_uuid(&uuid, 1) ){ |
| 1067 | fossil_redirect_home(); |
| 1068 | } |
| 1069 | zName = blob_str(&uuid); |
| 1070 | if( db_exists("SELECT 1 FROM ticket WHERE tkt_uuid='%s'", zName) ){ |
| 1071 | tktview_page(); |
| 1072 | return; |
| 1073 | } |
| 1074 | rid = db_int(0, "SELECT rid FROM blob WHERE uuid='%s'", zName); |
| 1075 | if( rid==0 ){ |
| 1076 | style_header("Broken Link"); |
| 1077 | @ <p>No such object: %h(zName)</p> |
| 1078 | style_footer(); |
| 1079 | return; |
| 1080 |
+43
-69
| --- src/name.c | ||
| +++ src/name.c | ||
| @@ -35,38 +35,33 @@ | ||
| 35 | 35 | ** This routine takes a user-entered UUID which might be in mixed |
| 36 | 36 | ** case and might only be a prefix of the full UUID and converts it |
| 37 | 37 | ** into the full-length UUID in canonical form. |
| 38 | 38 | ** |
| 39 | 39 | ** If the input is not a UUID or a UUID prefix, then try to resolve |
| 40 | -** the name as a tag. | |
| 40 | +** the name as a tag. If multiple tags match, pick the latest. | |
| 41 | 41 | ** |
| 42 | 42 | ** Return the number of errors. |
| 43 | 43 | */ |
| 44 | 44 | int name_to_uuid(Blob *pName, int iErrPriority){ |
| 45 | 45 | int rc; |
| 46 | 46 | int sz; |
| 47 | 47 | sz = blob_size(pName); |
| 48 | 48 | if( sz>UUID_SIZE || sz<4 || !validate16(blob_buffer(pName), sz) ){ |
| 49 | - Blob uuid; | |
| 50 | - static const char prefix[] = "tag:"; | |
| 51 | - static const int preflen = sizeof(prefix)-1; | |
| 49 | + char *zUuid; | |
| 52 | 50 | const char *zName = blob_str(pName); |
| 53 | - | |
| 54 | - if( strncmp(zName, prefix, preflen)==0 ){ | |
| 55 | - zName += preflen; | |
| 56 | - } | |
| 57 | - | |
| 58 | - sym_tag_to_uuid(zName, &uuid); | |
| 59 | - if( blob_size(&uuid)==0 ){ | |
| 60 | - fossil_error(iErrPriority, "not a valid object name: %s", zName); | |
| 61 | - blob_reset(&uuid); | |
| 62 | - return 1; | |
| 63 | - }else{ | |
| 51 | + if( memcmp(zName, "tag:", 4)==0 ){ | |
| 52 | + zName += 4; | |
| 53 | + } | |
| 54 | + zUuid = tag_to_uuid(zName); | |
| 55 | + if( zUuid ){ | |
| 64 | 56 | blob_reset(pName); |
| 65 | - *pName = uuid; | |
| 57 | + blob_append(pName, zUuid, -1); | |
| 58 | + free(zUuid); | |
| 66 | 59 | return 0; |
| 67 | 60 | } |
| 61 | + fossil_error(iErrPriority, "not a valid object name: %s", zName); | |
| 62 | + return 1; | |
| 68 | 63 | } |
| 69 | 64 | blob_materialize(pName); |
| 70 | 65 | canonical16(blob_buffer(pName), sz); |
| 71 | 66 | if( sz==UUID_SIZE ){ |
| 72 | 67 | rc = db_int(1, "SELECT 0 FROM blob WHERE uuid=%B", pName); |
| @@ -74,28 +69,29 @@ | ||
| 74 | 69 | fossil_error(iErrPriority, "no such artifact: %b", pName); |
| 75 | 70 | blob_reset(pName); |
| 76 | 71 | } |
| 77 | 72 | }else if( sz<UUID_SIZE && sz>=4 ){ |
| 78 | 73 | Stmt q; |
| 79 | - char zOrig[UUID_SIZE+1]; | |
| 80 | - memcpy(zOrig, blob_buffer(pName), sz); | |
| 81 | - zOrig[sz] = 0; | |
| 82 | - blob_reset(pName); | |
| 83 | - db_prepare(&q, "SELECT uuid FROM blob" | |
| 84 | - " WHERE uuid>='%s'" | |
| 85 | - " AND substr(uuid,1,%d)='%s'", | |
| 86 | - zOrig, sz, zOrig); | |
| 74 | + db_prepare(&q, "SELECT uuid FROM blob WHERE uuid GLOB '%b*'", pName); | |
| 87 | 75 | if( db_step(&q)!=SQLITE_ROW ){ |
| 76 | + char *zUuid; | |
| 88 | 77 | db_finalize(&q); |
| 89 | - fossil_error(iErrPriority, "no artifacts match the prefix \"%s\"", zOrig); | |
| 78 | + zUuid = tag_to_uuid(blob_str(pName)); | |
| 79 | + if( zUuid ){ | |
| 80 | + blob_reset(pName); | |
| 81 | + blob_append(pName, zUuid, -1); | |
| 82 | + free(zUuid); | |
| 83 | + return 0; | |
| 84 | + } | |
| 85 | + fossil_error(iErrPriority, "no artifacts match the prefix \"%b\"", pName); | |
| 90 | 86 | return 1; |
| 91 | 87 | } |
| 88 | + blob_reset(pName); | |
| 92 | 89 | blob_append(pName, db_column_text(&q, 0), db_column_bytes(&q, 0)); |
| 93 | 90 | if( db_step(&q)==SQLITE_ROW ){ |
| 94 | 91 | fossil_error(iErrPriority, |
| 95 | - "multiple artifacts match the prefix \"%s\"", | |
| 96 | - zOrig | |
| 92 | + "multiple artifacts match" | |
| 97 | 93 | ); |
| 98 | 94 | blob_reset(pName); |
| 99 | 95 | db_finalize(&q); |
| 100 | 96 | return 1; |
| 101 | 97 | } |
| @@ -106,52 +102,30 @@ | ||
| 106 | 102 | } |
| 107 | 103 | return rc; |
| 108 | 104 | } |
| 109 | 105 | |
| 110 | 106 | /* |
| 111 | -** This routine takes a name which might be a tag and attempts to | |
| 112 | -** produce a UUID. The UUID (if any) is returned in the blob pointed | |
| 113 | -** to by the second argument. | |
| 114 | -** | |
| 115 | -** Return as follows: | |
| 116 | -** 0 Name is not a tag | |
| 117 | -** 1 A single UUID was found | |
| 118 | -** 2 More than one UUID was found, so this is presumably a | |
| 119 | -** propagating tag. The return UUID is the most recent, | |
| 120 | -** which is most likely to be the one wanted. | |
| 121 | -*/ | |
| 122 | -int tag_to_uuid(const char *pName, Blob *pUuid,const char *pPrefix){ | |
| 123 | - Stmt q; | |
| 124 | - int count = 0; | |
| 125 | - db_prepare(&q, | |
| 126 | - "SELECT (SELECT uuid FROM blob WHERE rid=objid)" | |
| 127 | - " FROM tagxref JOIN event ON rid=objid" | |
| 128 | - " WHERE tagxref.tagid=(SELECT tagid FROM tag WHERE tagname=%Q||%Q)" | |
| 129 | - " AND tagtype>0" | |
| 130 | - " AND value IS NULL" | |
| 131 | - " ORDER BY event.mtime DESC", | |
| 132 | - pPrefix, | |
| 133 | - pName | |
| 134 | - ); | |
| 135 | - blob_zero(pUuid); | |
| 136 | - while( db_step(&q)==SQLITE_ROW ){ | |
| 137 | - count++; | |
| 138 | - if(count>1){ | |
| 139 | - break; | |
| 140 | - } | |
| 141 | - db_column_blob(&q, 0, pUuid); | |
| 142 | - } | |
| 143 | - db_finalize(&q); | |
| 144 | - return count; | |
| 145 | -} | |
| 146 | - | |
| 147 | -/* | |
| 148 | -** This routine takes a name which might be a symbolic tag and | |
| 149 | -** attempts to produce a UUID. See tag_to_uuid. | |
| 150 | -*/ | |
| 151 | -int sym_tag_to_uuid(const char *pName, Blob *pUuid){ | |
| 152 | - return tag_to_uuid(pName,pUuid,"sym-"); | |
| 107 | +** Convert a symbolic tag name into the UUID of a check-in that contains | |
| 108 | +** that tag. If the tag appears on multiple check-ins, return the UUID | |
| 109 | +** of the most recent check-in with the tag. | |
| 110 | +** | |
| 111 | +** Memory to hold the returned string comes from malloc() and needs to | |
| 112 | +** be freed by the caller. | |
| 113 | +*/ | |
| 114 | +char *tag_to_uuid(const char *zTag){ | |
| 115 | + char *zUuid = | |
| 116 | + db_text(0, | |
| 117 | + "SELECT blob.uuid" | |
| 118 | + " FROM tag, tagxref, event, blob" | |
| 119 | + " WHERE tag.tagname='sym-'||%Q " | |
| 120 | + " AND tagxref.tagid=tag.tagid AND tagxref.tagtype>0 " | |
| 121 | + " AND event.objid=tagxref.rid " | |
| 122 | + " AND blob.rid=event.objid " | |
| 123 | + " ORDER BY event.mtime DESC ", | |
| 124 | + zTag | |
| 125 | + ); | |
| 126 | + return zUuid; | |
| 153 | 127 | } |
| 154 | 128 | |
| 155 | 129 | /* |
| 156 | 130 | ** COMMAND: test-name-to-id |
| 157 | 131 | ** |
| 158 | 132 |
| --- src/name.c | |
| +++ src/name.c | |
| @@ -35,38 +35,33 @@ | |
| 35 | ** This routine takes a user-entered UUID which might be in mixed |
| 36 | ** case and might only be a prefix of the full UUID and converts it |
| 37 | ** into the full-length UUID in canonical form. |
| 38 | ** |
| 39 | ** If the input is not a UUID or a UUID prefix, then try to resolve |
| 40 | ** the name as a tag. |
| 41 | ** |
| 42 | ** Return the number of errors. |
| 43 | */ |
| 44 | int name_to_uuid(Blob *pName, int iErrPriority){ |
| 45 | int rc; |
| 46 | int sz; |
| 47 | sz = blob_size(pName); |
| 48 | if( sz>UUID_SIZE || sz<4 || !validate16(blob_buffer(pName), sz) ){ |
| 49 | Blob uuid; |
| 50 | static const char prefix[] = "tag:"; |
| 51 | static const int preflen = sizeof(prefix)-1; |
| 52 | const char *zName = blob_str(pName); |
| 53 | |
| 54 | if( strncmp(zName, prefix, preflen)==0 ){ |
| 55 | zName += preflen; |
| 56 | } |
| 57 | |
| 58 | sym_tag_to_uuid(zName, &uuid); |
| 59 | if( blob_size(&uuid)==0 ){ |
| 60 | fossil_error(iErrPriority, "not a valid object name: %s", zName); |
| 61 | blob_reset(&uuid); |
| 62 | return 1; |
| 63 | }else{ |
| 64 | blob_reset(pName); |
| 65 | *pName = uuid; |
| 66 | return 0; |
| 67 | } |
| 68 | } |
| 69 | blob_materialize(pName); |
| 70 | canonical16(blob_buffer(pName), sz); |
| 71 | if( sz==UUID_SIZE ){ |
| 72 | rc = db_int(1, "SELECT 0 FROM blob WHERE uuid=%B", pName); |
| @@ -74,28 +69,29 @@ | |
| 74 | fossil_error(iErrPriority, "no such artifact: %b", pName); |
| 75 | blob_reset(pName); |
| 76 | } |
| 77 | }else if( sz<UUID_SIZE && sz>=4 ){ |
| 78 | Stmt q; |
| 79 | char zOrig[UUID_SIZE+1]; |
| 80 | memcpy(zOrig, blob_buffer(pName), sz); |
| 81 | zOrig[sz] = 0; |
| 82 | blob_reset(pName); |
| 83 | db_prepare(&q, "SELECT uuid FROM blob" |
| 84 | " WHERE uuid>='%s'" |
| 85 | " AND substr(uuid,1,%d)='%s'", |
| 86 | zOrig, sz, zOrig); |
| 87 | if( db_step(&q)!=SQLITE_ROW ){ |
| 88 | db_finalize(&q); |
| 89 | fossil_error(iErrPriority, "no artifacts match the prefix \"%s\"", zOrig); |
| 90 | return 1; |
| 91 | } |
| 92 | blob_append(pName, db_column_text(&q, 0), db_column_bytes(&q, 0)); |
| 93 | if( db_step(&q)==SQLITE_ROW ){ |
| 94 | fossil_error(iErrPriority, |
| 95 | "multiple artifacts match the prefix \"%s\"", |
| 96 | zOrig |
| 97 | ); |
| 98 | blob_reset(pName); |
| 99 | db_finalize(&q); |
| 100 | return 1; |
| 101 | } |
| @@ -106,52 +102,30 @@ | |
| 106 | } |
| 107 | return rc; |
| 108 | } |
| 109 | |
| 110 | /* |
| 111 | ** This routine takes a name which might be a tag and attempts to |
| 112 | ** produce a UUID. The UUID (if any) is returned in the blob pointed |
| 113 | ** to by the second argument. |
| 114 | ** |
| 115 | ** Return as follows: |
| 116 | ** 0 Name is not a tag |
| 117 | ** 1 A single UUID was found |
| 118 | ** 2 More than one UUID was found, so this is presumably a |
| 119 | ** propagating tag. The return UUID is the most recent, |
| 120 | ** which is most likely to be the one wanted. |
| 121 | */ |
| 122 | int tag_to_uuid(const char *pName, Blob *pUuid,const char *pPrefix){ |
| 123 | Stmt q; |
| 124 | int count = 0; |
| 125 | db_prepare(&q, |
| 126 | "SELECT (SELECT uuid FROM blob WHERE rid=objid)" |
| 127 | " FROM tagxref JOIN event ON rid=objid" |
| 128 | " WHERE tagxref.tagid=(SELECT tagid FROM tag WHERE tagname=%Q||%Q)" |
| 129 | " AND tagtype>0" |
| 130 | " AND value IS NULL" |
| 131 | " ORDER BY event.mtime DESC", |
| 132 | pPrefix, |
| 133 | pName |
| 134 | ); |
| 135 | blob_zero(pUuid); |
| 136 | while( db_step(&q)==SQLITE_ROW ){ |
| 137 | count++; |
| 138 | if(count>1){ |
| 139 | break; |
| 140 | } |
| 141 | db_column_blob(&q, 0, pUuid); |
| 142 | } |
| 143 | db_finalize(&q); |
| 144 | return count; |
| 145 | } |
| 146 | |
| 147 | /* |
| 148 | ** This routine takes a name which might be a symbolic tag and |
| 149 | ** attempts to produce a UUID. See tag_to_uuid. |
| 150 | */ |
| 151 | int sym_tag_to_uuid(const char *pName, Blob *pUuid){ |
| 152 | return tag_to_uuid(pName,pUuid,"sym-"); |
| 153 | } |
| 154 | |
| 155 | /* |
| 156 | ** COMMAND: test-name-to-id |
| 157 | ** |
| 158 |
| --- src/name.c | |
| +++ src/name.c | |
| @@ -35,38 +35,33 @@ | |
| 35 | ** This routine takes a user-entered UUID which might be in mixed |
| 36 | ** case and might only be a prefix of the full UUID and converts it |
| 37 | ** into the full-length UUID in canonical form. |
| 38 | ** |
| 39 | ** If the input is not a UUID or a UUID prefix, then try to resolve |
| 40 | ** the name as a tag. If multiple tags match, pick the latest. |
| 41 | ** |
| 42 | ** Return the number of errors. |
| 43 | */ |
| 44 | int name_to_uuid(Blob *pName, int iErrPriority){ |
| 45 | int rc; |
| 46 | int sz; |
| 47 | sz = blob_size(pName); |
| 48 | if( sz>UUID_SIZE || sz<4 || !validate16(blob_buffer(pName), sz) ){ |
| 49 | char *zUuid; |
| 50 | const char *zName = blob_str(pName); |
| 51 | if( memcmp(zName, "tag:", 4)==0 ){ |
| 52 | zName += 4; |
| 53 | } |
| 54 | zUuid = tag_to_uuid(zName); |
| 55 | if( zUuid ){ |
| 56 | blob_reset(pName); |
| 57 | blob_append(pName, zUuid, -1); |
| 58 | free(zUuid); |
| 59 | return 0; |
| 60 | } |
| 61 | fossil_error(iErrPriority, "not a valid object name: %s", zName); |
| 62 | return 1; |
| 63 | } |
| 64 | blob_materialize(pName); |
| 65 | canonical16(blob_buffer(pName), sz); |
| 66 | if( sz==UUID_SIZE ){ |
| 67 | rc = db_int(1, "SELECT 0 FROM blob WHERE uuid=%B", pName); |
| @@ -74,28 +69,29 @@ | |
| 69 | fossil_error(iErrPriority, "no such artifact: %b", pName); |
| 70 | blob_reset(pName); |
| 71 | } |
| 72 | }else if( sz<UUID_SIZE && sz>=4 ){ |
| 73 | Stmt q; |
| 74 | db_prepare(&q, "SELECT uuid FROM blob WHERE uuid GLOB '%b*'", pName); |
| 75 | if( db_step(&q)!=SQLITE_ROW ){ |
| 76 | char *zUuid; |
| 77 | db_finalize(&q); |
| 78 | zUuid = tag_to_uuid(blob_str(pName)); |
| 79 | if( zUuid ){ |
| 80 | blob_reset(pName); |
| 81 | blob_append(pName, zUuid, -1); |
| 82 | free(zUuid); |
| 83 | return 0; |
| 84 | } |
| 85 | fossil_error(iErrPriority, "no artifacts match the prefix \"%b\"", pName); |
| 86 | return 1; |
| 87 | } |
| 88 | blob_reset(pName); |
| 89 | blob_append(pName, db_column_text(&q, 0), db_column_bytes(&q, 0)); |
| 90 | if( db_step(&q)==SQLITE_ROW ){ |
| 91 | fossil_error(iErrPriority, |
| 92 | "multiple artifacts match" |
| 93 | ); |
| 94 | blob_reset(pName); |
| 95 | db_finalize(&q); |
| 96 | return 1; |
| 97 | } |
| @@ -106,52 +102,30 @@ | |
| 102 | } |
| 103 | return rc; |
| 104 | } |
| 105 | |
| 106 | /* |
| 107 | ** Convert a symbolic tag name into the UUID of a check-in that contains |
| 108 | ** that tag. If the tag appears on multiple check-ins, return the UUID |
| 109 | ** of the most recent check-in with the tag. |
| 110 | ** |
| 111 | ** Memory to hold the returned string comes from malloc() and needs to |
| 112 | ** be freed by the caller. |
| 113 | */ |
| 114 | char *tag_to_uuid(const char *zTag){ |
| 115 | char *zUuid = |
| 116 | db_text(0, |
| 117 | "SELECT blob.uuid" |
| 118 | " FROM tag, tagxref, event, blob" |
| 119 | " WHERE tag.tagname='sym-'||%Q " |
| 120 | " AND tagxref.tagid=tag.tagid AND tagxref.tagtype>0 " |
| 121 | " AND event.objid=tagxref.rid " |
| 122 | " AND blob.rid=event.objid " |
| 123 | " ORDER BY event.mtime DESC ", |
| 124 | zTag |
| 125 | ); |
| 126 | return zUuid; |
| 127 | } |
| 128 | |
| 129 | /* |
| 130 | ** COMMAND: test-name-to-id |
| 131 | ** |
| 132 |