diff options
Diffstat (limited to 'apps/tagcache.c')
-rw-r--r-- | apps/tagcache.c | 144 |
1 files changed, 102 insertions, 42 deletions
diff --git a/apps/tagcache.c b/apps/tagcache.c index 753675f906..5ab77264f6 100644 --- a/apps/tagcache.c +++ b/apps/tagcache.c | |||
@@ -222,6 +222,8 @@ struct statefile_header { | |||
222 | 222 | ||
223 | /* Pointer to allocated ramcache_header */ | 223 | /* Pointer to allocated ramcache_header */ |
224 | static struct ramcache_header *ramcache_hdr; | 224 | static struct ramcache_header *ramcache_hdr; |
225 | /* lock entity to temporarily prevent ramcache_hdr from moving */ | ||
226 | static int move_lock; | ||
225 | #endif | 227 | #endif |
226 | 228 | ||
227 | /** | 229 | /** |
@@ -1035,6 +1037,8 @@ static bool check_clauses(struct tagcache_search *tcs, | |||
1035 | { | 1037 | { |
1036 | tfe = (struct tagfile_entry *) | 1038 | tfe = (struct tagfile_entry *) |
1037 | &ramcache_hdr->tags[clause->tag][seek]; | 1039 | &ramcache_hdr->tags[clause->tag][seek]; |
1040 | /* str points to movable data, but no locking required here, | ||
1041 | * as no yield() is following */ | ||
1038 | str = tfe->tag_data; | 1042 | str = tfe->tag_data; |
1039 | } | 1043 | } |
1040 | } | 1044 | } |
@@ -1149,9 +1153,11 @@ static bool build_lookup_list(struct tagcache_search *tcs) | |||
1149 | # endif | 1153 | # endif |
1150 | ) | 1154 | ) |
1151 | { | 1155 | { |
1156 | move_lock++; /* lock because below makes a pointer to movable data */ | ||
1152 | for (i = tcs->seek_pos; i < current_tcmh.tch.entry_count; i++) | 1157 | for (i = tcs->seek_pos; i < current_tcmh.tch.entry_count; i++) |
1153 | { | 1158 | { |
1154 | struct tagcache_seeklist_entry *seeklist; | 1159 | struct tagcache_seeklist_entry *seeklist; |
1160 | /* idx points to movable data, don't yield or reload */ | ||
1155 | struct index_entry *idx = &ramcache_hdr->indices[i]; | 1161 | struct index_entry *idx = &ramcache_hdr->indices[i]; |
1156 | if (tcs->seek_list_count == SEEK_LIST_SIZE) | 1162 | if (tcs->seek_list_count == SEEK_LIST_SIZE) |
1157 | break ; | 1163 | break ; |
@@ -1175,8 +1181,7 @@ static bool build_lookup_list(struct tagcache_search *tcs) | |||
1175 | /* Check for conditions. */ | 1181 | /* Check for conditions. */ |
1176 | if (!check_clauses(tcs, idx, tcs->clause, tcs->clause_count)) | 1182 | if (!check_clauses(tcs, idx, tcs->clause, tcs->clause_count)) |
1177 | continue; | 1183 | continue; |
1178 | 1184 | /* Add to the seek list if not already in uniq buffer (doesn't yield)*/ | |
1179 | /* Add to the seek list if not already in uniq buffer. */ | ||
1180 | if (!add_uniqbuf(tcs, idx->tag_seek[tcs->type])) | 1185 | if (!add_uniqbuf(tcs, idx->tag_seek[tcs->type])) |
1181 | continue; | 1186 | continue; |
1182 | 1187 | ||
@@ -1187,6 +1192,7 @@ static bool build_lookup_list(struct tagcache_search *tcs) | |||
1187 | seeklist->idx_id = i; | 1192 | seeklist->idx_id = i; |
1188 | tcs->seek_list_count++; | 1193 | tcs->seek_list_count++; |
1189 | } | 1194 | } |
1195 | move_lock--; | ||
1190 | 1196 | ||
1191 | tcs->seek_pos = i; | 1197 | tcs->seek_pos = i; |
1192 | 1198 | ||
@@ -1538,10 +1544,11 @@ static bool get_next(struct tagcache_search *tcs) | |||
1538 | struct tagfile_entry *ep; | 1544 | struct tagfile_entry *ep; |
1539 | 1545 | ||
1540 | ep = (struct tagfile_entry *)&ramcache_hdr->tags[tcs->type][tcs->position]; | 1546 | ep = (struct tagfile_entry *)&ramcache_hdr->tags[tcs->type][tcs->position]; |
1541 | tcs->result = ep->tag_data; | 1547 | /* don't return ep->tag_data directly as it may move */ |
1542 | tcs->result_len = strlen(tcs->result) + 1; | 1548 | tcs->result_len = strlcpy(buf, ep->tag_data, sizeof(buf)) + 1; |
1549 | tcs->result = buf; | ||
1543 | tcs->idx_id = ep->idx_id; | 1550 | tcs->idx_id = ep->idx_id; |
1544 | tcs->ramresult = true; | 1551 | tcs->ramresult = false; /* was true before we copied to buf too */ |
1545 | 1552 | ||
1546 | /* Increase position for the next run. This may get overwritten. */ | 1553 | /* Increase position for the next run. This may get overwritten. */ |
1547 | tcs->position += sizeof(struct tagfile_entry) + ep->tag_length; | 1554 | tcs->position += sizeof(struct tagfile_entry) + ep->tag_length; |
@@ -1703,15 +1710,34 @@ bool tagcache_fill_tags(struct mp3entry *id3, const char *filename) | |||
1703 | entry = &ramcache_hdr->indices[idx_id]; | 1710 | entry = &ramcache_hdr->indices[idx_id]; |
1704 | 1711 | ||
1705 | memset(id3, 0, sizeof(struct mp3entry)); | 1712 | memset(id3, 0, sizeof(struct mp3entry)); |
1706 | 1713 | char* buf = id3->id3v2buf; | |
1707 | id3->title = get_tag_string(entry, tag_title); | 1714 | ssize_t remaining = sizeof(id3->id3v2buf); |
1708 | id3->artist = get_tag_string(entry, tag_artist); | 1715 | |
1709 | id3->album = get_tag_string(entry, tag_album); | 1716 | /* this macro sets id3 strings by copying to the id3v2buf */ |
1710 | id3->genre_string = get_tag_string(entry, tag_genre); | 1717 | #define SET(x, y) do \ |
1711 | id3->composer = get_tag_string(entry, tag_composer); | 1718 | { \ |
1712 | id3->comment = get_tag_string(entry, tag_comment); | 1719 | if (remaining > 0) \ |
1713 | id3->albumartist = get_tag_string(entry, tag_albumartist); | 1720 | { \ |
1714 | id3->grouping = get_tag_string(entry, tag_grouping); | 1721 | x = NULL; /* initialize with null if tag doesn't exist */ \ |
1722 | char* src = get_tag_string(entry, y); \ | ||
1723 | if (src) \ | ||
1724 | { \ | ||
1725 | x = buf; \ | ||
1726 | size_t len = strlcpy(buf, src, remaining) +1; \ | ||
1727 | buf += len; remaining -= len; \ | ||
1728 | } \ | ||
1729 | } \ | ||
1730 | } while(0) | ||
1731 | |||
1732 | |||
1733 | SET(id3->title, tag_title); | ||
1734 | SET(id3->artist, tag_artist); | ||
1735 | SET(id3->album, tag_album); | ||
1736 | SET(id3->genre_string, tag_genre); | ||
1737 | SET(id3->composer, tag_composer); | ||
1738 | SET(id3->comment, tag_comment); | ||
1739 | SET(id3->albumartist, tag_albumartist); | ||
1740 | SET(id3->grouping, tag_grouping); | ||
1715 | 1741 | ||
1716 | id3->length = get_tag_numeric(entry, tag_length, idx_id); | 1742 | id3->length = get_tag_numeric(entry, tag_length, idx_id); |
1717 | id3->playcount = get_tag_numeric(entry, tag_playcount, idx_id); | 1743 | id3->playcount = get_tag_numeric(entry, tag_playcount, idx_id); |
@@ -2903,6 +2929,9 @@ static bool commit(void) | |||
2903 | #ifdef HAVE_DIRCACHE | 2929 | #ifdef HAVE_DIRCACHE |
2904 | bool dircache_buffer_stolen = false; | 2930 | bool dircache_buffer_stolen = false; |
2905 | #endif | 2931 | #endif |
2932 | #ifdef HAVE_TC_RAMCACHE | ||
2933 | bool ramcache_buffer_stolen = false; | ||
2934 | #endif | ||
2906 | bool local_allocation = false; | 2935 | bool local_allocation = false; |
2907 | 2936 | ||
2908 | logf("committing tagcache"); | 2937 | logf("committing tagcache"); |
@@ -2976,6 +3005,8 @@ static bool commit(void) | |||
2976 | tempbuf = (char *)(ramcache_hdr + 1); | 3005 | tempbuf = (char *)(ramcache_hdr + 1); |
2977 | tempbuf_size = tc_stat.ramcache_allocated - sizeof(struct ramcache_header) - 128; | 3006 | tempbuf_size = tc_stat.ramcache_allocated - sizeof(struct ramcache_header) - 128; |
2978 | tempbuf_size &= ~0x03; | 3007 | tempbuf_size &= ~0x03; |
3008 | move_lock++; | ||
3009 | ramcache_buffer_stolen = true; | ||
2979 | } | 3010 | } |
2980 | #endif | 3011 | #endif |
2981 | 3012 | ||
@@ -3072,6 +3103,8 @@ static bool commit(void) | |||
3072 | #endif | 3103 | #endif |
3073 | 3104 | ||
3074 | #ifdef HAVE_TC_RAMCACHE | 3105 | #ifdef HAVE_TC_RAMCACHE |
3106 | if (ramcache_buffer_stolen) | ||
3107 | move_lock--; | ||
3075 | /* Reload tagcache. */ | 3108 | /* Reload tagcache. */ |
3076 | if (tc_stat.ramcache_allocated > 0) | 3109 | if (tc_stat.ramcache_allocated > 0) |
3077 | tagcache_start_scan(); | 3110 | tagcache_start_scan(); |
@@ -3689,9 +3722,11 @@ static bool delete_entry(long idx_id) | |||
3689 | { | 3722 | { |
3690 | struct tagfile_entry *tfe; | 3723 | struct tagfile_entry *tfe; |
3691 | int32_t *seek = &ramcache_hdr->indices[idx_id].tag_seek[tag]; | 3724 | int32_t *seek = &ramcache_hdr->indices[idx_id].tag_seek[tag]; |
3692 | 3725 | ||
3693 | tfe = (struct tagfile_entry *)&ramcache_hdr->tags[tag][*seek]; | 3726 | tfe = (struct tagfile_entry *)&ramcache_hdr->tags[tag][*seek]; |
3727 | move_lock++; /* protect tfe and seek if crc_32() yield()s */ | ||
3694 | *seek = crc_32(tfe->tag_data, strlen(tfe->tag_data), 0xffffffff); | 3728 | *seek = crc_32(tfe->tag_data, strlen(tfe->tag_data), 0xffffffff); |
3729 | move_lock--; | ||
3695 | myidx.tag_seek[tag] = *seek; | 3730 | myidx.tag_seek[tag] = *seek; |
3696 | } | 3731 | } |
3697 | else | 3732 | else |
@@ -3813,6 +3848,30 @@ static bool check_event_queue(void) | |||
3813 | #endif | 3848 | #endif |
3814 | 3849 | ||
3815 | #ifdef HAVE_TC_RAMCACHE | 3850 | #ifdef HAVE_TC_RAMCACHE |
3851 | |||
3852 | static void fix_ramcache(void* old_addr, void* new_addr) | ||
3853 | { | ||
3854 | ptrdiff_t offpos = new_addr - old_addr; | ||
3855 | for (int i = 0; i < TAG_COUNT; i++) | ||
3856 | ramcache_hdr->tags[i] += offpos; | ||
3857 | } | ||
3858 | |||
3859 | static int move_cb(int handle, void* current, void* new) | ||
3860 | { | ||
3861 | (void)handle; | ||
3862 | if (move_lock > 0) | ||
3863 | return BUFLIB_CB_CANNOT_MOVE; | ||
3864 | |||
3865 | fix_ramcache(current, new); | ||
3866 | ramcache_hdr = new; | ||
3867 | return BUFLIB_CB_OK; | ||
3868 | } | ||
3869 | |||
3870 | static struct buflib_callbacks ops = { | ||
3871 | .move_callback = move_cb, | ||
3872 | .shrink_callback = NULL, | ||
3873 | }; | ||
3874 | |||
3816 | static bool allocate_tagcache(void) | 3875 | static bool allocate_tagcache(void) |
3817 | { | 3876 | { |
3818 | struct master_header tcmh; | 3877 | struct master_header tcmh; |
@@ -3833,7 +3892,7 @@ static bool allocate_tagcache(void) | |||
3833 | */ | 3892 | */ |
3834 | tc_stat.ramcache_allocated = tcmh.tch.datasize + 256 + TAGCACHE_RESERVE + | 3893 | tc_stat.ramcache_allocated = tcmh.tch.datasize + 256 + TAGCACHE_RESERVE + |
3835 | sizeof(struct ramcache_header) + TAG_COUNT*sizeof(void *); | 3894 | sizeof(struct ramcache_header) + TAG_COUNT*sizeof(void *); |
3836 | int handle = core_alloc("tc ramcache", tc_stat.ramcache_allocated); | 3895 | int handle = core_alloc_ex("tc ramcache", tc_stat.ramcache_allocated, &ops); |
3837 | ramcache_hdr = core_get_data(handle); | 3896 | ramcache_hdr = core_get_data(handle); |
3838 | memset(ramcache_hdr, 0, sizeof(struct ramcache_header)); | 3897 | memset(ramcache_hdr, 0, sizeof(struct ramcache_header)); |
3839 | memcpy(¤t_tcmh, &tcmh, sizeof current_tcmh); | 3898 | memcpy(¤t_tcmh, &tcmh, sizeof current_tcmh); |
@@ -3871,12 +3930,13 @@ static bool tagcache_dumpload(void) | |||
3871 | 3930 | ||
3872 | 3931 | ||
3873 | /* Lets allocate real memory and load it */ | 3932 | /* Lets allocate real memory and load it */ |
3874 | handle = core_alloc("tc ramcache", shdr.tc_stat.ramcache_allocated); | 3933 | handle = core_alloc_ex("tc ramcache", shdr.tc_stat.ramcache_allocated, &ops); |
3875 | ramcache_hdr = core_get_data(handle); | 3934 | ramcache_hdr = core_get_data(handle); |
3935 | moev_lock++; | ||
3876 | rc = read(fd, ramcache_hdr, shdr.tc_stat.ramcache_allocated); | 3936 | rc = read(fd, ramcache_hdr, shdr.tc_stat.ramcache_allocated); |
3937 | move_lock--; | ||
3877 | close(fd); | 3938 | close(fd); |
3878 | 3939 | ||
3879 | offpos = (long)ramcache_hdr - (long)shdr.hdr; | ||
3880 | if (rc != shdr.tc_stat.ramcache_allocated) | 3940 | if (rc != shdr.tc_stat.ramcache_allocated) |
3881 | { | 3941 | { |
3882 | logf("read failure!"); | 3942 | logf("read failure!"); |
@@ -3887,8 +3947,7 @@ static bool tagcache_dumpload(void) | |||
3887 | memcpy(&tc_stat, &shdr.tc_stat, sizeof(struct tagcache_stat)); | 3947 | memcpy(&tc_stat, &shdr.tc_stat, sizeof(struct tagcache_stat)); |
3888 | 3948 | ||
3889 | /* Now fix the pointers */ | 3949 | /* Now fix the pointers */ |
3890 | for (i = 0; i < TAG_COUNT; i++) | 3950 | fix_ramcache(shdr.hdr, ramcache_hdr); |
3891 | ramcache_hdr->tags[i] += offpos; | ||
3892 | 3951 | ||
3893 | /* Load the tagcache master header (should match the actual DB file header). */ | 3952 | /* Load the tagcache master header (should match the actual DB file header). */ |
3894 | memcpy(¤t_tcmh, &shdr.mh, sizeof current_tcmh); | 3953 | memcpy(¤t_tcmh, &shdr.mh, sizeof current_tcmh); |
@@ -3919,7 +3978,9 @@ static bool tagcache_dumpsave(void) | |||
3919 | write(fd, &shdr, sizeof shdr); | 3978 | write(fd, &shdr, sizeof shdr); |
3920 | 3979 | ||
3921 | /* And dump the data too */ | 3980 | /* And dump the data too */ |
3981 | move_lock++; | ||
3922 | write(fd, ramcache_hdr, tc_stat.ramcache_allocated); | 3982 | write(fd, ramcache_hdr, tc_stat.ramcache_allocated); |
3983 | move_lock--; | ||
3923 | close(fd); | 3984 | close(fd); |
3924 | 3985 | ||
3925 | return true; | 3986 | return true; |
@@ -3962,7 +4023,8 @@ static bool load_tagcache(void) | |||
3962 | 4023 | ||
3963 | /* Master header copy should already match, this can be redundant to do. */ | 4024 | /* Master header copy should already match, this can be redundant to do. */ |
3964 | memcpy(¤t_tcmh, &tcmh, sizeof current_tcmh); | 4025 | memcpy(¤t_tcmh, &tcmh, sizeof current_tcmh); |
3965 | 4026 | ||
4027 | move_lock++; /* lock for the reset of the scan, simpler to handle */ | ||
3966 | idx = ramcache_hdr->indices; | 4028 | idx = ramcache_hdr->indices; |
3967 | 4029 | ||
3968 | /* Load the master index table. */ | 4030 | /* Load the master index table. */ |
@@ -3972,8 +4034,7 @@ static bool load_tagcache(void) | |||
3972 | if (bytesleft < 0) | 4034 | if (bytesleft < 0) |
3973 | { | 4035 | { |
3974 | logf("too big tagcache."); | 4036 | logf("too big tagcache."); |
3975 | close(fd); | 4037 | goto failure; |
3976 | return false; | ||
3977 | } | 4038 | } |
3978 | 4039 | ||
3979 | /* DEBUG: After tagcache commit and dircache rebuild, hdr-sturcture | 4040 | /* DEBUG: After tagcache commit and dircache rebuild, hdr-sturcture |
@@ -3982,8 +4043,7 @@ static bool load_tagcache(void) | |||
3982 | if (rc != sizeof(struct index_entry)) | 4043 | if (rc != sizeof(struct index_entry)) |
3983 | { | 4044 | { |
3984 | logf("read error #10"); | 4045 | logf("read error #10"); |
3985 | close(fd); | 4046 | goto failure; |
3986 | return false; | ||
3987 | } | 4047 | } |
3988 | 4048 | ||
3989 | idx++; | 4049 | idx++; |
@@ -4010,7 +4070,7 @@ static bool load_tagcache(void) | |||
4010 | p += sizeof(struct tagcache_header); | 4070 | p += sizeof(struct tagcache_header); |
4011 | 4071 | ||
4012 | if ( (fd = open_tag_fd(tch, tag, false)) < 0) | 4072 | if ( (fd = open_tag_fd(tch, tag, false)) < 0) |
4013 | return false; | 4073 | goto failure_nofd; |
4014 | 4074 | ||
4015 | for (ramcache_hdr->entry_count[tag] = 0; | 4075 | for (ramcache_hdr->entry_count[tag] = 0; |
4016 | ramcache_hdr->entry_count[tag] < tch->entry_count; | 4076 | ramcache_hdr->entry_count[tag] < tch->entry_count; |
@@ -4022,7 +4082,7 @@ static bool load_tagcache(void) | |||
4022 | { | 4082 | { |
4023 | /* Abort if we got a critical event in queue */ | 4083 | /* Abort if we got a critical event in queue */ |
4024 | if (check_event_queue()) | 4084 | if (check_event_queue()) |
4025 | return false; | 4085 | goto failure; |
4026 | } | 4086 | } |
4027 | 4087 | ||
4028 | fe = (struct tagfile_entry *)p; | 4088 | fe = (struct tagfile_entry *)p; |
@@ -4032,8 +4092,7 @@ static bool load_tagcache(void) | |||
4032 | { | 4092 | { |
4033 | /* End of lookup table. */ | 4093 | /* End of lookup table. */ |
4034 | logf("read error #11"); | 4094 | logf("read error #11"); |
4035 | close(fd); | 4095 | goto failure; |
4036 | return false; | ||
4037 | } | 4096 | } |
4038 | 4097 | ||
4039 | /* We have a special handling for the filename tags. */ | 4098 | /* We have a special handling for the filename tags. */ |
@@ -4051,16 +4110,14 @@ static bool load_tagcache(void) | |||
4051 | buf[10] = '\0'; | 4110 | buf[10] = '\0'; |
4052 | logf("TAG:%s", buf); | 4111 | logf("TAG:%s", buf); |
4053 | logf("too long filename"); | 4112 | logf("too long filename"); |
4054 | close(fd); | 4113 | goto failure; |
4055 | return false; | ||
4056 | } | 4114 | } |
4057 | 4115 | ||
4058 | rc = read(fd, buf, fe->tag_length); | 4116 | rc = read(fd, buf, fe->tag_length); |
4059 | if (rc != fe->tag_length) | 4117 | if (rc != fe->tag_length) |
4060 | { | 4118 | { |
4061 | logf("read error #12"); | 4119 | logf("read error #12"); |
4062 | close(fd); | 4120 | goto failure; |
4063 | return false; | ||
4064 | } | 4121 | } |
4065 | 4122 | ||
4066 | /* Check if the entry has already been removed */ | 4123 | /* Check if the entry has already been removed */ |
@@ -4071,15 +4128,13 @@ static bool load_tagcache(void) | |||
4071 | if (idx->flag & FLAG_DIRCACHE) | 4128 | if (idx->flag & FLAG_DIRCACHE) |
4072 | { | 4129 | { |
4073 | logf("internal error!"); | 4130 | logf("internal error!"); |
4074 | close(fd); | 4131 | goto failure; |
4075 | return false; | ||
4076 | } | 4132 | } |
4077 | 4133 | ||
4078 | if (idx->tag_seek[tag] != pos) | 4134 | if (idx->tag_seek[tag] != pos) |
4079 | { | 4135 | { |
4080 | logf("corrupt data structures!"); | 4136 | logf("corrupt data structures!"); |
4081 | close(fd); | 4137 | goto failure; |
4082 | return false; | ||
4083 | } | 4138 | } |
4084 | 4139 | ||
4085 | # ifdef HAVE_DIRCACHE | 4140 | # ifdef HAVE_DIRCACHE |
@@ -4126,8 +4181,7 @@ static bool load_tagcache(void) | |||
4126 | logf("too big tagcache #2"); | 4181 | logf("too big tagcache #2"); |
4127 | logf("tl: %ld", fe->tag_length); | 4182 | logf("tl: %ld", fe->tag_length); |
4128 | logf("bl: %ld", bytesleft); | 4183 | logf("bl: %ld", bytesleft); |
4129 | close(fd); | 4184 | goto failure; |
4130 | return false; | ||
4131 | } | 4185 | } |
4132 | 4186 | ||
4133 | p = fe->tag_data; | 4187 | p = fe->tag_data; |
@@ -4141,8 +4195,7 @@ static bool load_tagcache(void) | |||
4141 | logf("len=0x%04lx", fe->tag_length); // 0x4000 | 4195 | logf("len=0x%04lx", fe->tag_length); // 0x4000 |
4142 | logf("pos=0x%04lx", lseek(fd, 0, SEEK_CUR)); // 0x433 | 4196 | logf("pos=0x%04lx", lseek(fd, 0, SEEK_CUR)); // 0x433 |
4143 | logf("tag=0x%02x", tag); // 0x00 | 4197 | logf("tag=0x%02x", tag); // 0x00 |
4144 | close(fd); | 4198 | goto failure; |
4145 | return false; | ||
4146 | } | 4199 | } |
4147 | } | 4200 | } |
4148 | close(fd); | 4201 | close(fd); |
@@ -4151,7 +4204,14 @@ static bool load_tagcache(void) | |||
4151 | tc_stat.ramcache_used = tc_stat.ramcache_allocated - bytesleft; | 4204 | tc_stat.ramcache_used = tc_stat.ramcache_allocated - bytesleft; |
4152 | logf("tagcache loaded into ram!"); | 4205 | logf("tagcache loaded into ram!"); |
4153 | 4206 | ||
4207 | move_lock--; | ||
4154 | return true; | 4208 | return true; |
4209 | |||
4210 | failure: | ||
4211 | close(fd); | ||
4212 | failure_nofd: | ||
4213 | move_lock--; | ||
4214 | return false; | ||
4155 | } | 4215 | } |
4156 | #endif /* HAVE_TC_RAMCACHE */ | 4216 | #endif /* HAVE_TC_RAMCACHE */ |
4157 | 4217 | ||