/* * $Id: dbck.c,v 0.6 1991/09/15 10:32:29 linus Exp $ * Copyright (C) 1991 Lysator Academic Computer Association. * * This file is part of the LysKOM server. * * LysKOM is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 1, or (at your option) * any later version. * * LysKOM is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * You should have received a copy of the GNU General Public License * along with LysKOM; see the file COPYING. If not, write to * Lysator, c/o ISY, Linkoping University, S-581 83 Linkoping, SWEDEN, * or the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, * MA 02139, USA. * * Please mail bug reports to bug-lyskom@lysator.liu.se. */ /* * dbck.c - A simple database checker and corrector. * * Author: Per Cederqvist. */ static char *rcsid = "$Id: dbck.c,v 0.6 1991/09/15 10:32:29 linus Exp $"; #include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <kom-types.h> #include <config.h> #include "cache.h" #include "log.h" #include "lyskomd.h" #include "misc-parser.h" #include <server/smalloc.h> #include <debug.h> #include "dbck-cache.h" char datafilename[1024]; /* Full pathname to the database file */ char backupfilename[1024]; /* Full pathname to the backup file */ char textfilename[1024]; char textbackupfilename[1024]; /* unshrinked text-file. */ static const char *dbase_dir = NULL; /* Directory where database resides */ #define TEXTBACKUPFILE_NAME "db/backup-texts" int vflag=0; /* Verbose - list statistics also. */ int iflag=0; /* Interactive - prompt user and repair. */ int rflag=0; /* Repair simple error without confirmation. */ int gflag=0; /* Garbage collect text-file. */ int modifications = 0; typedef struct { int created_confs; } Person_scratchpad; static const Person_scratchpad EMPTY_PERSON_SCRATCHPAD = { 0 }; #include "tmp-limits.h" static Person_scratchpad *person_scratchpad[MAX_CONF]; #ifdef DEBUG int buglevel = 0; #endif extern void log (const char * format, ...) { va_list AP; va_start(AP, format); vfprintf(stdout, format, AP); va_end(AP); } extern void restart_kom (const char * format, ...) { va_list AP; va_start(AP, format); vfprintf(stdout, format, AP); va_end(AP); exit(1); } static Person_scratchpad * alloc_person_scratchpad(void) { Person_scratchpad *p; p = smalloc(sizeof(Person_scratchpad)); *p = EMPTY_PERSON_SCRATCHPAD; return p; } static Bool is_comment_to(Text_no comment, Text_stat *parent) { int i; for ( i = 0; i < parent->no_of_misc; i++ ) { switch( parent->misc_items[ i ].type ) { case comm_in: if ( parent->misc_items[ i ].datum.commented_in == comment ) return TRUE; break; default: break; } } return FALSE; } static Bool is_commented_in(Text_no parent, Text_stat *child) { int i; for ( i = 0; i < child->no_of_misc; i++ ) { switch( child->misc_items[ i ].type ) { case comm_to: if ( child->misc_items[ i ].datum.comment_to == parent ) return TRUE; break; default: break; } } return FALSE; } static Bool is_footnote_to(Text_no footnote, Text_stat *parent) { int i; for ( i = 0; i < parent->no_of_misc; i++ ) { switch( parent->misc_items[ i ].type ) { case footn_in: if ( parent->misc_items[ i ].datum.footnoted_in == footnote ) return TRUE; break; default: break; } } return FALSE; } static Bool is_footnoted_in(Text_no parent, Text_stat *child) { int i; for ( i = 0; i < child->no_of_misc; i++ ) { switch( child->misc_items[ i ].type ) { case footn_to: if ( child->misc_items[ i ].datum.footnote_to == parent ) return TRUE; break; default: break; } } return FALSE; } Member * locate_member(Pers_no pers_no, Conference * conf_c) { Member * member; int i; for(member = conf_c->members.members, i = conf_c->members.no_of_members; i > 0; i--, member++) { if ( member->member == pers_no ) { return member; } } return NULL; } /* * Delete a misc_info. * If it is a recpt, cc_recpt, comm_to or footn_to delete any * loc_no, rec_time, sent_by or sent_at that might follow it. * * Note that the Misc_info is not reallocated. */ static void delete_misc (Text_stat *tstat, Misc_info *misc) /* Pointer to first misc_item to delete. */ { int del = 1; /* Number of items to delete. */ /* Always delete at least one item. */ Bool ready; /* Check range of misc */ if (misc < tstat->misc_items || misc >= tstat->misc_items + tstat->no_of_misc ) { restart_kom("delete_misc() - misc out of range"); } ready = FALSE; while (ready == FALSE && misc + del < tstat->misc_items + tstat->no_of_misc ) { switch ( misc[ del ].type ) { case loc_no: case rec_time: case sent_by: case sent_at: del++; break; case recpt: case cc_recpt: case footn_to: case footn_in: case comm_to: case comm_in: ready = TRUE; break; #ifndef COMPILE_CHECKS default: restart_kom("delete_misc() - illegal misc found.\n"); #endif } } tstat->no_of_misc -= del; /* Move items beyond the deleted ones. */ while ( misc < tstat->misc_items + tstat->no_of_misc ) { misc[ 0 ] = misc[ del ]; misc++; } } static int confirm(char *question) { if ( iflag ) { fputs(question, stdout); fputs(" (y/n) ", stdout); while(1) switch(getchar()) { case 'y': case 'Y': return 1; case 'n': case 'N': case EOF: return 0; default: break; } } else return 0; } static long check_misc_infos(Text_no tno, Text_stat *tstat) { const Misc_info * misc = tstat->misc_items; Misc_info * previous; Misc_info_group group; Conference *c; Text_stat *t; long error=0; while (previous = (Misc_info *)misc, group = parse_next_misc(&misc, tstat->misc_items + tstat->no_of_misc), group.type != m_end_of_list && group.type != m_error ) { switch ( group.type ) { case m_recpt: c = cached_get_conf_stat (group.recipient); if ( c == NULL && group.recipient == 0 ) { log ("Conference 0 is recipient to text %lu.\n", (u_long)tno); if (rflag || confirm("Repair by deleting misc_item? ")) { delete_misc(tstat, previous); mark_text_as_changed(tno); modifications++; log("Repaired: Conference 0 is no longer a recipient.\n"); misc = previous; } else error++; break; } if ( c == NULL ) break; /* Check loc_no */ if ( group.local_no < c->texts.first_local_no ) { log("Text %lu: Recipient %lu<%lu> loc_no is less than %lu\n", (u_long)tno, (u_long)group.recipient, (u_long)group.local_no, (u_long)c->texts.first_local_no); error++; } else if ( c->texts.first_local_no + c->texts.no_of_texts - 1 < group.local_no ) { log("Text %lu: Recipient %lu<%lu> loc_no" " is greater than %lu\n", (u_long)tno, (u_long)group.recipient, (u_long)group.local_no, (u_long)(c->texts.first_local_no + c->texts.no_of_texts - 1)); error++; } else if ( c->texts.texts[group.local_no - c->texts.first_local_no] != tno ) { log("Text %lu: Recipient %lu<%lu>: that local number " "is mapped to %lu.\n", (u_long)tno, (u_long)group.recipient, (u_long)group.local_no, (u_long)c->texts.texts[group.local_no - c->texts.first_local_no]); error++; } break; case m_cc_recpt: c = cached_get_conf_stat (group.cc_recipient); if ( c == NULL && group.cc_recipient == 0 ) { log ("Conference 0 is cc_recipient to text %lu.\n", (u_long)tno); if (rflag || confirm("Repair by deleting misc_item? ")) { delete_misc(tstat, previous); mark_text_as_changed(tno); modifications++; log("Repaired: Conference 0 is no longer " "a cc_recipient.\n"); misc = previous; } else error++; break; } if ( c == NULL ) break; /* Check loc_no */ if ( group.local_no < c->texts.first_local_no ) { log("Text %lu: CC_Recipient %lu<%lu> is less than %lu\n", (u_long)tno, (u_long)group.cc_recipient, (u_long)group.local_no, (u_long)c->texts.first_local_no); error++; } else if ( c->texts.first_local_no + c->texts.no_of_texts - 1 < group.local_no ) { log("Text %lu: CC_Recipient %lu<%lu> loc_no is " "greater than %lu\n", (u_long)tno, (u_long)group.cc_recipient, (u_long)group.local_no, (u_long)(c->texts.first_local_no + c->texts.no_of_texts - 1)); error++; } else if ( c->texts.texts[group.local_no - c->texts.first_local_no] != tno ) { log("Text %lu: CC_Recipient %lu<%lu>: that local " "number is mapped to %lu.\n", (u_long)tno, (u_long)group.cc_recipient, (u_long)group.local_no, (u_long)c->texts.texts[group.local_no - c->texts.first_local_no]); error++; } break; case m_comm_to: t = cached_get_text_stat(group.comment_to); if ( t == NULL ) { log("Text %lu is a comment to %lu, which doesn't exist.\n", (u_long)tno, (u_long)group.comment_to); if (rflag || confirm("Repair by deleting misc_item? ")) { delete_misc(tstat, previous); mark_text_as_changed(tno); modifications++; log("Repaired: Comment-link deleted.\n"); misc = previous; } else error++; error++; } else if (!is_comment_to(tno, t)) { log("Text %lu is a comment to %lu, but not the reverse.\n", (u_long)tno, (u_long)group.comment_to); error++; } break; case m_comm_in: t = cached_get_text_stat(group.commented_in); if ( t == NULL ) { log("Text %lu is commented in %lu, which doesn't exist.\n", (u_long)tno, (u_long)group.commented_in); if (rflag || confirm("Repair by deleting misc_item? ")) { delete_misc(tstat, previous); mark_text_as_changed(tno); modifications++; log("Repaired: Comment-link deleted.\n"); misc = previous; } else error++; } else if (!is_commented_in(tno, t)) { log("Text %lu is a comment to %lu, but not the reverse.\n", (u_long)tno, (u_long)group.commented_in); error++; } break; case m_footn_to: t = cached_get_text_stat(group.footnote_to); if ( t == NULL ) { log("Text %lu is a footnote to %lu, which doesn't exist.\n", (u_long)tno, (u_long)group.footnote_to); if (rflag || confirm("Repair by deleting misc_item? ")) { delete_misc(tstat, previous); mark_text_as_changed(tno); modifications++; log("Repaired: Footnote-link deleted.\n"); misc = previous; } else error++; } else if (!is_footnote_to(tno, t)) { log("Text %lu is a footnote to %lu, but not the reverse.\n", (u_long)tno, (u_long)group.footnote_to); error++; } break; case m_footn_in: t = cached_get_text_stat(group.footnoted_in); if ( t == NULL ) { log("Text %lu is footnoted in %lu, which doesn't exist.\n", (u_long)tno, (u_long)group.footnoted_in); if (rflag || confirm("Repair by deleting misc_item? ")) { delete_misc(tstat, previous); mark_text_as_changed(tno); modifications++; log("Repaired: Footnote-link deleted.\n"); misc = previous; } else error++; } else if (!is_footnoted_in(tno, t)) { log("Text %lu is a footnot to %lu, but not the reverse.\n", (u_long)tno, (u_long)group.footnoted_in); error++; } break; default: log("check_misc_infos(): parse_next_misc returned type %lu\n", (u_long)group.type); break; } } if ( group.type == m_error ) { log("Text %lu has a bad misc_info_list.\n", (u_long)tno); error++; } return error; } static long check_texts(void) { Text_no ct = 0; Text_stat *ctp=NULL; long errors = 0; Text_no number_of_texts = 0; u_long bytes=0; u_long max_bytes=0; Text_no max_text=0; while ( ct = traverse_text(ct) ) { number_of_texts++; ctp = cached_get_text_stat( ct ); if ( ctp == NULL ) { log("Text %lu nonexistent.\n", ct); errors++; } else { bytes += ctp->no_of_chars; if ( ctp->no_of_chars > max_bytes ) { max_bytes = ctp->no_of_chars; max_text = ct; } /* no_of_marks is not yet checked. */ errors += check_misc_infos(ct, ctp); } } if (vflag) { if ( number_of_texts == 0 ) log("WARNING: No texts found.\n"); else { log("Total of %lu texts (total %lu bytes, " "average %lu bytes/text).\n", (u_long)number_of_texts, (u_long)bytes, (u_long)(bytes/number_of_texts)); log("Longest text is %lu (%lu bytes).\n", (u_long)max_text, (u_long)max_bytes); } } return errors; } static Bool adjust_text_list(Text_list *text_list) { u_long zeroes; u_long i; for (zeroes = 0; zeroes < text_list->no_of_texts && text_list->texts[ zeroes ] == 0; zeroes++) ; if ( zeroes > 0 ) { text_list->no_of_texts -= zeroes; text_list->first_local_no += zeroes; for ( i = 0; i < text_list->no_of_texts; i++) text_list->texts[ i ] = text_list->texts[ i + zeroes ]; text_list->texts = srealloc(text_list->texts, (text_list->no_of_texts * sizeof(Text_no))); } return zeroes > 0; } static int check_created_texts(Pers_no pno, Text_list *created) { u_long i; Text_stat *t; int error=0; for ( i=0; i < created->no_of_texts; i++ ) { if (created->texts[i] != 0) { t = cached_get_text_stat(created->texts[i]); if ( t != NULL && t->author != pno) { log("Person %lu is author of text %lu whose author is %lu.\n", (u_long)pno, (u_long)created->texts[i], (u_long)t->author); error++; } if ( t == NULL ) { log("Person %lu is author of text %lu, which doesn't exist.\n", (u_long)pno, (u_long)created->texts[i]); if ( rflag || confirm("Repair by setting to text_no to 0 in local map")) { created->texts[i] = 0; mark_person_as_changed(pno); modifications++; log("Repaired: created_texts corrected.\n"); } else error++; } } } if ( created->no_of_texts > 0 && created->texts[0] == 0 ) { log("Person %lu has a bad created_texts array. Starts with a 0.\n", (u_long)pno); if ( rflag || confirm ("Repair by adjusting created_texts")) { adjust_text_list(created); mark_person_as_changed(pno); modifications++; log("Repaired: created_texts adjusted.\n"); } else error++; } return error; } static int check_membership(Pers_no pno, const Membership *mship) { int error=0; Conference *conf; int i; Local_text_no last=0; conf = cached_get_conf_stat(mship->conf_no); if ( conf == NULL ) { log("Person %lu is a member in the non-existing conference %lu.\n", (u_long)pno, (u_long)mship->conf_no); error++; } else { /* Check read texts */ if ( mship->last_text_read > conf->texts.first_local_no + conf->texts.no_of_texts - 1) { log("Person %lu has read text %lu in conf %lu, " "which only has %lu texts.\n", (u_long)pno, (u_long)mship->last_text_read, (u_long)mship->conf_no, (u_long)(conf->texts.first_local_no + conf->texts.no_of_texts - 1)); error++; } for ( last = i = 0; i < mship->no_of_read; i++) { if ( mship->read_texts[i] <= last ) { log("Person %lu's membership in %lu is corrupt:" " read text number %lu<%lu> <= %lu.\n", (u_long)pno, (u_long)mship->conf_no, (u_long)mship->read_texts[i], (u_long)i, (u_long)last); error++; } last = mship->read_texts[i]; } /* Check that he is a member */ if ( locate_member(pno, conf) == NULL ) { log("Person %lu is a member in %lu in which he isn't a member.\n", (u_long)pno, (u_long)mship->conf_no); error++; } } return error; } static int check_membership_list(Pers_no pno, const Membership_list *mlist) { int errors=0; int i; for (i = 0; i < mlist->no_of_confs; i++) errors += check_membership(pno, &mlist->confs[i]); return errors; } static int check_persons(void) { Pers_no cp = 0; Person *pstat=NULL; Conference *cstat=NULL; long errors = 0; Pers_no number_of_persons=0; while ( cp = traverse_person(cp) ) { number_of_persons++; pstat = cached_get_person_stat (cp); cstat = cached_get_conf_stat (cp); if ( pstat == NULL ) { log("Person %lu nonexistent.\n", (u_long)cp); errors++; } else if (cstat == NULL) { log("Person %lu has no conference.\n", (u_long)cp); errors++; } else if (!cstat->type.letter_box) { log("Person %lu's conference is not a letter_box.\n", (u_long)cp); errors++; } else { errors += (check_created_texts(cp, &pstat->created_texts) + check_membership_list(cp, &pstat->conferences)); } } if (vflag) log("Total of %lu persons.\n", number_of_persons); return errors; } static Bool is_recipient(Conf_no conf_no, Text_stat * t_stat) { int i; for ( i = 0; i < t_stat->no_of_misc; i++ ) { switch( t_stat->misc_items[ i ].type ) { case recpt: if ( t_stat->misc_items[ i ].datum.recipient == conf_no ) { return TRUE; } break; case cc_recpt: if ( t_stat->misc_items[ i ].datum.cc_recipient == conf_no ) { return TRUE; } break; case rec_time: case comm_to: case comm_in: case footn_to: case footn_in: case sent_by: case sent_at: case loc_no: break; #ifndef COMPILE_CHECKS default: restart_kom("is_recipient(): illegal misc_item\n"); #endif } } return FALSE; } static int check_texts_in_conf(Conf_no cc, Text_list *tlist) { u_long i; Text_stat *t; int error=0; for ( i=0; i < tlist->no_of_texts; i++ ) { if (tlist->texts[i] != 0) { t = cached_get_text_stat(tlist->texts[i]); if ( t == NULL ) { log("Text %lu<%lu> in conference %lu is non-existent.\n", (u_long)tlist->texts[i], (u_long)i + tlist->first_local_no, (u_long)cc); if (rflag || confirm("Repair by setting Text_no to 0 in the map?") ) { tlist->texts[i]=0; mark_conference_as_changed(cc); modifications++; log("Repaired: %lu is no longer a recipient.\n", (u_long)cc); } else error++; } else { if ( !is_recipient(cc, t) ) { log("Text %lu<%lu> in conference %lu doesn't " "have the conference as recipient.\n", (u_long)tlist->texts[i], (u_long)i + tlist->first_local_no, (u_long)cc); if (confirm("Repair by setting Text_no to 0 in the map?") ) { tlist->texts[i]=0; mark_conference_as_changed(cc); modifications++; log("Repaired: %lu is no longer a recipient.\n", (u_long)cc); } else error++; } } } } if ( tlist->no_of_texts > 0 && tlist->texts[0] == 0 ) { log("Conference %lu has a bad Text_list. Starts with a 0.\n", (u_long)cc); if ( rflag || confirm ("Repair by adjusting text_list")) { adjust_text_list(tlist); mark_conference_as_changed(cc); modifications++; log("Repaired: text_list adjusted.\n"); } else error++; } return error; } Membership * locate_membership(Conf_no conf_no, Person * pers_p) { Membership * confp; int i; for(confp = pers_p->conferences.confs, i = pers_p->conferences.no_of_confs; i > 0; i--, confp++) { if ( confp->conf_no == conf_no ) { return confp; } } return NULL; } static int check_member(Conf_no cc, Member *memb) { Person *pp; int error=0; pp = cached_get_person_stat(memb->member); if ( pp == NULL ) { log("Person %lu, who is supposed to be a member in conf %lu, " "is nonexistent.\n", (u_long)memb->member, (u_long)cc); error++; } else { if ( locate_membership(cc, pp) == NULL ) { log("Person %lu is not a member in conf %lu.\n", (u_long)memb->member, (u_long)cc); error++; } } return error; } static int check_member_list(Conf_no cc, const Member_list *mlist) { int errors=0; int i; for (i = 0; i < mlist->no_of_members; i++) errors += check_member(cc, &mlist->members[i]); return errors; } static int check_confs(void) { Conf_no cc = 0; Person *pstat=NULL; Conference *cstat=NULL; long errors = 0; Conf_no number_of_confs = 0; while ( (cc = traverse_conference(cc)) != 0 ) { number_of_confs++; cstat = cached_get_conf_stat (cc); if ( cstat == NULL ) { log("Conference %lu nonexistent.\n", (u_long)cc); errors++; } else { if (cstat->type.letter_box) { pstat = cached_get_person_stat(cc); if (pstat == NULL) { log("Mailbox %lu has no person.\n", (u_long)cc); errors++; } } else /* not letter_box */ { /* Remember that the creator might no longer exist. */ if ( person_scratchpad[ cstat->creator ] != NULL ) ++person_scratchpad[ cstat->creator ]->created_confs; } errors += (check_texts_in_conf(cc, &cstat->texts) + check_member_list(cc, &cstat->members)); } } if ( vflag ) log("Total of %lu conferences.\n", (u_long)number_of_confs); return errors; } static void init_person_scratch(void) { Pers_no pno = 0; while( (pno = traverse_person(pno)) != 0 ) { person_scratchpad[pno] = alloc_person_scratchpad(); } } static long post_check_persons(void) { long errors = 0; Pers_no pers_no = 0; Person *pstat; while ( (pers_no = traverse_person(pers_no)) != 0 ) { if ( (pstat = cached_get_person_stat(pers_no)) == NULL ) { log("INTERNAL DBCK ERROR: post_check_persons(): can't " "cached_get_person_stat(%d).\n", pers_no); } if ( person_scratchpad[pers_no]->created_confs != pstat->created_confs ) { log("Person %d has created %d conferences, not %d (as said in " "his person-stat).\n", pers_no, person_scratchpad[pers_no]->created_confs, pstat->created_confs); if ( rflag || confirm("Repair by altering person-stat? ") ) { pstat->created_confs = person_scratchpad[pers_no]->created_confs; mark_person_as_changed(pers_no); modifications++; log("Person-stat corrected.\n"); } else errors++; } } return errors; } /* * Returns 0 if the database seems to be correct. */ static long check_data_base(void) { long errors; init_person_scratch(); errors = check_texts() + check_persons() + check_confs(); return errors + post_check_persons(); } static void init_data_base(void) { if (dbase_dir == NULL) dbase_dir = DEFAULT_DBASE_DIR; sprintf(datafilename, "%s/%s", dbase_dir, DATAFILE_NAME); sprintf(backupfilename, "%s/%s", dbase_dir, BACKUPFILE_NAME); sprintf(textfilename, "%s/%s", dbase_dir, TEXTFILE_NAME); sprintf(textbackupfilename, "%s/%s", dbase_dir, TEXTBACKUPFILE_NAME); if ( vflag ) { log("Database = %s\n", datafilename); log("Backup = %s\n", backupfilename); log("Text = %s\n", textfilename); log("Textback = %s\n", textbackupfilename); } if ( init_cache() == FAILURE ) restart_kom("Can't find database.\n"); } void garb_text_file(void) { Text_no tno = 0; String text; log("Renaming %s to %s\n", textfilename, textbackupfilename); rename(textfilename, textbackupfilename); log("Writing texts to (new) %s\n", textfilename); fflush(stdout); fflush(stderr); cache_open_new_text_file(); while ( (tno = traverse_text(tno)) != 0 ) { text = cached_get_text(tno); cached_flush_text(tno, text); free_tmp(); } log("Writing datafile with new indexes.\n"); fflush(stdout); fflush(stderr); cache_sync(); log("Ready."); } int main (int argc, char **argv) { int i; int errors; BUGDECL; for (i = 1; i < argc && argv[i][0] == '-'; i++) { switch (argv[i][1]) { #ifdef DEBUG case 'd': buglevel++; break; #endif case 'D': /* Database directory */ dbase_dir = argv[i]+2; break; case 'i': /* Running interactively. */ iflag++; /* Will ask user and try to repair. */ break; case 'r': /* Repair simple errors wihtout asking. */ rflag++; break; case 'v': /* Verbose: report more than errors. */ vflag++; break; case 'g': /* Garbage collect: compress text-file. */ gflag++; break; default: restart_kom("usage: dbck [options]\n"); } } s_set_storage_management(smalloc, srealloc, sfree); init_data_base(); errors = check_data_base(); if ( iflag ) log("Total of %d error%s remains.\n", errors, errors == 1 ? "" : "s"); else if ( vflag && errors > 0 ) log("%d error%s found.\n", errors, errors == 1 ? "" : "s"); if ( modifications > 0 ) { log("%d modification%s made. Syncing...\n", modifications, modifications == 1 ? "" : "s"); fflush(stdout); fflush(stderr); cache_sync(); log("ready.\n"); } if ( modifications == 0 && errors == 0 && gflag ) { log("No errors found. Compressing textfile.\n"); fflush(stdout); fflush(stderr); garb_text_file(); log("ready.\n"); } return errors != 0; }