/*---------------------------------------------------------------------------* Project: functions to delete files from archive File: delete.c Copyright (C) 2001-2006 Nintendo All rights reserved. These coded instructions, statements, and computer programs contain proprietary information of Nintendo of America Inc. and/or Nintendo Company Ltd., and are protected by Federal copyright law. They may not be disclosed to third parties or copied or duplicated in any form, in whole or in part, without the prior written consent of Nintendo. $Log: create.c,v $ Revision 1.3 2007/11/13 06:59:53 nakano_yoshinobu Fixed path names of files in .arc file. Revision 1.2 2006/05/15 05:15:06 kawaset header.reserve[] is explicitly cleared in ConstructFSTFromStructure(). Revision 1.1 2006/04/20 01:41:22 hiratsu Initial check-in. 3 03/05/01 15:01 Hashida Fixed two bugs. - A bug that darchD.exe crashes if same files are specified to the argument. - A bug that darchD.exe malfunctions if a directory/file name is a part of other directory/file name that is listed before the name. 1 7/02/01 11:34p Hashida Initial revision. $NoKeywords: $ *---------------------------------------------------------------------------*/ #include "darch.h" extern DarchHandle* OpenArc(char* fileName); extern BOOL GetNextEntry(DarchHandle* handle, FSTEntryInfo* entryInfo); extern BOOL CloseArc(DarchHandle* handle); static char CurrDir[FILENAME_MAX]; typedef struct DirStructure DirStructure; struct DirStructure { DirStructure* next; DirStructure* prev; DirStructure* child; DirStructure* parent; char pathName[FILENAME_MAX]; // file name in archive char* name; int nameOffset; DirStructure* file; BOOL isDir; int entrynum; int numItems; DirStructure* nextInFst; DirStructure* nextInDisc; // only valid for files DirStructure* prevInDisc; // only valid for files char dirName[FILENAME_MAX]; char fileName[FILENAME_MAX]; // file name in the real world int fileLength; int filePosition; int fileNewPosition; }; DirStructure DiskStart; DirStructure* DiskLastPtr = &DiskStart; void InsertInDiscList(DirStructure* item) { DirStructure* s; DirStructure* p; p = &DiskStart; for (s = DiskStart.nextInDisc; s; s = s->nextInDisc) { p = s; if (item->fileNewPosition < s->fileNewPosition) { s->prevInDisc->nextInDisc = item; item->prevInDisc = s->prevInDisc; s->prevInDisc = item; item->nextInDisc = s; return; } } item->nextInDisc = NULL; p->nextInDisc = item; item->prevInDisc = p; return; } /*---------------------------------------------------------------------------* Name: CreateHierarchy Description: Create hierarchy using DirStructure. Arguments: handle handle for archive currDir current directory (= parent directory for directories we are checking) nextEntry start of next directory (i.e.nextEntry-1 is the end of currDir) Returns: None *---------------------------------------------------------------------------*/ void CreateHierarchy(DarchHandle* handle, DirStructure* currDir, int nextEntry) { DirStructure* lastDir; DirStructure* item; DirStructure* lastFile; int firstDir = 1; int firstFile = 1; FSTEntryInfo entryInfo; // If handle->currEntry == nextEntry - 1, then the next GetNextEntry will // return item that is not under this directory, so we shouldn't proceed while(handle->currEntry < nextEntry - 1) { // this GetNextEntry should never return NULL GetNextEntry(handle, &entryInfo); if ( NULL == (item = (DirStructure*)malloc(sizeof(DirStructure))) ) { fprintf(stderr, "%s: Malloc failed\n", progName); exit(1); } strcpy(item->pathName, entryInfo.pathName); item->nameOffset = entryInfo.nameOffset; item->name = item->pathName + item->nameOffset; item->next = NULL; item->parent = currDir; item->child = NULL; item->file = NULL; if (item->isDir = entryInfo.isDir) { if (firstDir) { currDir->child = item; item->prev = NULL; firstDir = 0; } else { lastDir->next = item; item->prev = lastDir; } lastDir = item; CreateHierarchy(handle, item, entryInfo.nextEntry); } else { if (firstFile) { currDir->file = item; item->prev = NULL; firstFile = 0; } else { lastFile->next = item; item->prev = lastFile; } lastFile = item; strcpy(item->dirName, "."); strcpy(item->fileName, handle->name); item->filePosition = entryInfo.filePosition; item->fileNewPosition = entryInfo.filePosition; item->fileLength = entryInfo.fileLength; InsertInDiscList(item); } } } /*---------------------------------------------------------------------------* Name: freeDir Description: free memory allocated for directory structure recursively Arguments: item dir structure to free Returns: None *---------------------------------------------------------------------------*/ void freeDir(DirStructure* item) { DirStructure* child; DirStructure* file; DirStructure* tmp; for(child = item->child; child != NULL;) { tmp = child->next; freeDir(child); child = tmp; } for(file = item->file; file != NULL;) { file->prevInDisc->nextInDisc = file->nextInDisc; if (file->nextInDisc) { file->nextInDisc->prevInDisc = file->prevInDisc; } tmp = file->next; free(file); file = tmp; } free(item); } /*---------------------------------------------------------------------------* Name: DeleteItemFromStructure Description: Delete specified item from file structure Arguments: item dir structure to delete Returns: None *---------------------------------------------------------------------------*/ void DeleteItemFromStructure(DirStructure* item) { DirStructure* prevItem; DirStructure* nextItem; DirStructure* parent; prevItem = item->prev; if (prevItem == NULL) { parent = item->parent; if (item->isDir) parent->child = item->next; else parent->file = item->next; } else { prevItem->next = item->next; } nextItem = item->next; if (nextItem) nextItem->prev = prevItem; if (!item->isDir) { item->prevInDisc->nextInDisc = item->nextInDisc; if (item->nextInDisc) { item->nextInDisc->prevInDisc = item->prevInDisc; } } if (item->isDir) freeDir(item); else free(item); } /*---------------------------------------------------------------------------* Name: MyStrnCmp Description: Compare two strings up to maxlen Arguments: str1 string 1 str2 string 2 maxlen maximum length to compare Returns: 0 if same, 1 otherwise *---------------------------------------------------------------------------*/ static int MyStrnCmp(char* str1, char* str2, u32 maxlen) { u32 i = maxlen; if (maxlen == 0) return 1; for (i = 0; i < maxlen; i++) { if (tolower(*str1) != tolower(*str2)) return 1; if (*str1 == '\0') return 0; str1++; str2++; } return 0; } /*---------------------------------------------------------------------------* Name: FindMatchingItemName Description: Get directory structure that has /name/ under dir Arguments: dir directory structure. Search dir/file under this dir name name of the directory/file (updated if the first char of the string is "/") count only count chars are compared from /name/ Returns: dir structure is returned if any is matched. NULL is returned otherwise. *---------------------------------------------------------------------------*/ DirStructure* FindMatchingItemName(DirStructure* dir, char* name, int count) { DirStructure* item; // Note that name member of item is null-terminated. for(item = dir->child; item != NULL; item = item->next) { if ( (MyStrnCmp(item->name, name, count) == 0) && (strlen(item->name) == count) ) return item; } for(item = dir->file; item != NULL; item = item->next) { if ( (MyStrnCmp(item->name, name, count) == 0) && (strlen(item->name) == count) ) return item; } return NULL; } /*---------------------------------------------------------------------------* Name: GetFirstDirCount Description: Get length of the first directory/file name Arguments: name name of the directory/file (updated if the first char of the string is "/") Returns: -1 if name is NULL or just "/". Length of the first directory/ file name otherwise *---------------------------------------------------------------------------*/ int GetFirstDirCount(char** name) { char* ptr; // Ignore leading '/' if any if (**name == '/') (*name)++; // if the first letter is end mark, return -1 if (**name == '\0') return -1; ptr = *name; while( (*ptr != '/') && (*ptr != '\0') ) ptr++; return (ptr - *name); } /*---------------------------------------------------------------------------* Name: Delete Description: Detete specified name from file structure Arguments: name name of the directory/file to delete root root dir for the structure Returns: None *---------------------------------------------------------------------------*/ void Delete(char* name, DirStructure* root) { DirStructure* curr; DirStructure* item; int n; char* nameptr; nameptr = name; curr = root; // if the name is "" or "/", do nothing n = GetFirstDirCount(&nameptr); if (n == -1) return; while(1) { // if /curr/ is a file, it means it matched to a file but there's // some strings still left to compare, which is bad if ( (!curr->isDir) || ( (item = FindMatchingItemName(curr, nameptr, n)) == NULL ) ) { fprintf(stderr, "%s: %s: Not found in archive\n", progName, name); exit(1); } nameptr += n; n = GetFirstDirCount(&nameptr); if (n == -1) break; curr = item; } DeleteItemFromStructure(item); } void InitializeRootDir(DirStructure* root) { root->next = NULL; root->prev = NULL; root->child = NULL; root->parent = root; root->pathName[0] = '\0'; root->name = root->pathName; root->nameOffset = 0; root->file = NULL; root->isDir = TRUE; root->entrynum = 0; } /*---------------------------------------------------------------------------* Name: ConstructStructureFromFST Description: Create hierarchy using DirStructure. Arguments: handle DarchHandle structure for the archive file root root dir for the structure Returns: None *---------------------------------------------------------------------------*/ void ConstructStructureFromFST(DarchHandle* handle, DirStructure* root) { root->next = NULL; root->prev = NULL; root->child = NULL; root->parent = root; root->pathName[0] = '\0'; root->name = root->pathName; root->nameOffset = 0; root->file = NULL; root->isDir = TRUE; root->entrynum = 0; DiskStart.nextInDisc = NULL; DiskStart.prevInDisc = NULL; CreateHierarchy(handle, root, handle->entryNum); } /*---------------------------------------------------------------------------* Name: DecideOrderInFst Description: Make chain of items in the order in FST Arguments: start the starting directory to enum numItems [out]number of items including /start/ charLength [out]sum of names of all items under start including /start/ Returns: last item of items under the directory *---------------------------------------------------------------------------*/ DirStructure* DecideOrderInFst(DirStructure* start, int* numItems, int* charLength) { int chars; DirStructure* curr; DirStructure* last; int subTotal, subCharLength; int count; // we start from 1 because the count includes /start/ count = 1; chars = strlen(start->name) + 1; last = start; if (start->file) curr = start->file; else curr = start->child; while(curr) { last->nextInFst = curr; if (curr->isDir) { last = DecideOrderInFst(curr, &subTotal, &subCharLength); curr->numItems = subTotal; count += subTotal; chars += subCharLength; curr = curr->next; } else { last = curr; count++; chars += strlen(curr->name) + 1; if (curr->next) curr = curr->next; else curr = curr->parent->child; } } *numItems = count; *charLength = chars; return last; } void GetArcNewName(char* newname, char* oldname) { char* ptr; ptr = strrchr(oldname, '/'); if (ptr == NULL) { ptr = strrchr(oldname, '\\'); } if (ptr != NULL) { ptr++; } else { ptr = oldname; } strncpy(newname, oldname, ptr-oldname); newname[ptr-oldname] = '\0'; strcat(newname, "___"); strcat(newname, ptr); strcat(newname, ".new"); } void DeterminPositionInDisc(int userPos) { DirStructure* item; for (item = &DiskStart; item->nextInDisc; ) { item = item->nextInDisc; item->fileNewPosition = userPos; userPos += RoundUp32B(item->fileLength); } } /*---------------------------------------------------------------------------* Name: ConstructFSTFromStructure Description: Construct FST from structure and make new archive Arguments: arcName archive name root root dir for the structure Returns: None *---------------------------------------------------------------------------*/ void ConstructFSTFromStructure(char* arcName, DirStructure* root) { int total; int charLength, numItems; DirStructure* item; char* FSTStringStart; char* charPtr; FSTEntry* currFSTEntry; int fidOld, fidNew; char arcNewName[FILENAME_MAX]; void* fst; int i; int userPos; int fstPos; int fstLength; char currDirectory[FILENAME_MAX]; ARCHeader header; if (GetCurrentDirectory(FILENAME_MAX, currDirectory) >= FILENAME_MAX) { fprintf(stderr, "%s: Assertion error\n", progName); exit(1); } GetArcNewName(arcNewName, arcName); if( (fidNew = open(arcNewName, O_BINARY | O_TRUNC | O_CREAT | O_WRONLY, 0666)) == -1 ) { fprintf(stderr, "%s: Error opening %s\n", progName, arcNewName); exit(1); } DecideOrderInFst(root, &numItems, &charLength); root->numItems = total = numItems; fstLength = total * sizeof(FSTEntry) + charLength; if ( NULL == (fst = malloc(fstLength)) ) { fprintf(stderr, "%s: Malloc failed\n", progName); exit(1); } currFSTEntry = (FSTEntry*)fst; charPtr = FSTStringStart = (char*)(currFSTEntry + total); item = root; fstPos = sizeof(ARCHeader); userPos = RoundUp32B(fstPos + fstLength); DeterminPositionInDisc(userPos); header.magic = REV32(DARCH_MAGIC); header.fstStart = REV32(fstPos); header.fstSize = REV32(fstLength); header.fileStart = REV32(userPos); header.reserve[0] = 0; header.reserve[1] = 0; header.reserve[2] = 0; header.reserve[3] = 0; for(i = 0; i < total; i++) { item->entrynum = i; setIsDir(currFSTEntry, item->isDir); setStringOff(currFSTEntry, charPtr - FSTStringStart); if (debugMode) { fprintf(stderr, "pathname: %s\n", item->pathName); fprintf(stderr, "name: %s\n", item->name); } strcpy(charPtr, item->name); charPtr += strlen(item->name) + 1; if (item->isDir) { setParent(currFSTEntry, item->parent->entrynum); setNextEntry(currFSTEntry, (i + item->numItems)); } else { // setPosition(currFSTEntry, userPos); setPosition(currFSTEntry, item->fileNewPosition); setLength(currFSTEntry, item->fileLength); if(SetCurrentDirectory(item->dirName) == 0) { fprintf(stderr, "%s: Error when changing curr directory to %s\n", progName, item->dirName); fprintf(stderr, "Error code is %d\n", GetLastError()); exit(1); } if( (fidOld = open(item->fileName, O_BINARY | O_RDONLY)) == -1 ) { fprintf(stderr, "%s: Error opening %s\n", progName, item->fileName); exit(1); } CopyUtility(fidOld, item->filePosition, fidNew, item->fileNewPosition, item->fileLength); close(fidOld); // XXX need to be aligned in 4bytes for disk access? // userPos += item->fileLength; } item = item->nextInFst; currFSTEntry++; } // write fst if ( -1 == lseek(fidNew, fstPos, SEEK_SET) ) { fprintf(stderr, "%s: lseek failed on archive\n", progName); exit(1); } if (fstLength != write(fidNew, fst, fstLength)) { fprintf(stderr, "%s: Cannot write archive\n", progName); exit (1); } // write header if ( -1 == lseek(fidNew, 0, SEEK_SET) ) { fprintf(stderr, "%s: lseek failed on archive\n", progName); exit(1); } if (sizeof(ARCHeader) != write(fidNew, &header, sizeof(ARCHeader))) { fprintf(stderr, "%s: Cannot write archive\n", progName); exit (1); } close(fidNew); if(SetCurrentDirectory(currDirectory) == 0) { fprintf(stderr, "%s: Error when changing curr directory to %s\n", progName, currDirectory); fprintf(stderr, "Error code is %d\n", GetLastError()); exit(1); } // we ignore error message if (unlink(arcName) == -1) { if (errno != ENOENT) { fprintf(stderr, "%s: Error unlinking %s\n", progName, arcName); perror("unlink"); exit(1); } } if (MoveFile(arcNewName, arcName) == 0) { fprintf(stderr, "%s: Error renaming %s to %s\n", progName, arcNewName, arcName); fprintf(stderr, "Error code is %d\n", GetLastError()); exit(1); } } /*---------------------------------------------------------------------------* Name: DeleteArc Description: Delete files from archive Arguments: arcFile archive name arg file list to delete count num of list Returns: None *---------------------------------------------------------------------------*/ void DeleteArc(char* arcFile, Arg_s* arg, int count) { int i, j; DirStructure rootDir; DarchHandle* handle; handle = OpenArc(arcFile); ConstructStructureFromFST(handle, &rootDir); CloseArc(handle); for (i = 0; i < count; i++) { for (j = 0; j < arg[i].argNum; j++) { Delete((arg[i].argStart)[j], &rootDir); } } ConstructFSTFromStructure(arcFile, &rootDir); } /*---------------------------------------------------------------------------* Name: CreateHierarchyFromFilesRecursively Description: search under /currDir/ to create hierarchy Arguments: currDir directory to search. Returns: None *---------------------------------------------------------------------------*/ void CreateHierarchyFromFilesRecursively(DirStructure* currDir) { WIN32_FIND_DATA findData; HANDLE dirHandle; DirStructure* lastDir; DirStructure* item; DirStructure* lastFile; int firstDir = 1; int firstFile = 1; char nameToFind[FILENAME_MAX]; BOOL result; int error; strcpy(nameToFind, currDir->fileName); strcat(nameToFind, "/*"); /* dummy comment for emacs */ if (debugMode) { fprintf(stderr, "Searching %s\n", nameToFind); } dirHandle = FindFirstFile(nameToFind, &findData); if (dirHandle == INVALID_HANDLE_VALUE) { fprintf(stderr, "Failed when opening %s\n", currDir->fileName); fprintf(stderr, "Error code is %d\n", GetLastError()); exit(1); } // skip . and .. (Find FirstFile already read ".") result = FindNextFile(dirHandle, &findData); // read ".." if (!result) { fprintf(stderr, "Failed when reading %s\n", currDir->fileName); fprintf(stderr, "Error code is %d\n", GetLastError()); exit(1); } while(1) { result = FindNextFile(dirHandle, &findData); if (!result) { error = GetLastError(); if (error == ERROR_NO_MORE_FILES) { // prepare to return from the function if (debugMode) { fprintf(stderr, "no more files or subdirectories in this directory\n"); } return; } else { fprintf(stderr, "Error occurred when reading directory %s\n", currDir->fileName); fprintf(stderr, "Error code is %d\n", error); exit(1); } } if ( NULL == (item = (DirStructure*)malloc(sizeof(DirStructure))) ) { fprintf(stderr, "%s: Malloc failed\n", progName); exit(1); } strcpy(item->pathName, currDir->pathName); strcat(item->pathName, "/"); item->nameOffset = strlen(item->pathName); strcat(item->pathName, findData.cFileName); item->name = item->pathName + item->nameOffset; strcpy(item->dirName, currDir->dirName); strcpy(item->fileName, currDir->fileName); strcat(item->fileName, "/"); strcat(item->fileName, findData.cFileName); item->next = NULL; item->parent = currDir; item->child = NULL; item->file = NULL; item->isDir = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)? TRUE : FALSE; if (item->isDir) { if (firstDir) { currDir->child = item; item->prev = NULL; firstDir = 0; } else { lastDir->next = item; item->prev = lastDir; } lastDir = item; CreateHierarchyFromFilesRecursively(item); } else { if (firstFile) { currDir->file = item; item->prev = NULL; firstFile = 0; } else { lastFile->next = item; item->prev = lastFile; } lastFile = item; item->filePosition = 0; item->fileNewPosition = 0; if ( (findData.nFileSizeHigh != 0) || (findData.nFileSizeLow >= 0x80000000) ) { fprintf(stderr, "%s: File %s is too large\n", progName, item->fileName); exit(1); } item->fileLength = findData.nFileSizeLow; DiskLastPtr->nextInDisc = item; item->prevInDisc = DiskLastPtr; DiskLastPtr = item; } } } static void AddItem(DirStructure* parent, DirStructure* item) { DirStructure* curr; DirStructure** start; item->parent = parent; if (item->isDir) start = &(parent->child); else start = &(parent->file); if (*start == NULL) { *start = item; item->next = NULL; item->prev = NULL; } else { // find last item of the chain for(curr = *start; curr->next; curr = curr->next) ; curr->next = item; item->next = NULL; item->prev = curr; } return; } void MergeDirStructure(DirStructure* root1, DirStructure* root2) { DirStructure* item; DirStructure* matched; DirStructure* next; for(item = root2->child; item; item = item->next) { next = item->next; matched = FindMatchingItemName(root1, item->name, strlen(item->name)); if (matched == NULL) AddItem(root1, item); else { if (matched->isDir) { MergeDirStructure(matched, item); } else { fprintf(stderr, "%s: %s: File exists\n", progName, root1->pathName); exit(1); } } } for(item = root2->file; item; item = next) { next = item->next; matched = FindMatchingItemName(root1, item->name, strlen(item->name)); if (matched == NULL) AddItem(root1, item); else { if (matched->isDir) { fprintf(stderr, "%s: %s: Directory exists\n", progName, root1->pathName); exit(1); } else { fprintf(stderr, "%s: %s: File exists\n", progName, root1->pathName); exit(1); } } } } #define END_OF_PATH(ptr) \ ( ((ptr)[0] == '\0') || ( ((ptr)[0] == '/') && ((ptr)[1] == '\0') ) ) DirStructure* CreateHierarchyFromFiles(char* dirName, char* name) { DirStructure* root; DirStructure* last; DirStructure* item; int attr; int error; char* nameptr; int next; struct stat sb; int pathOffset = 0; assert(name); if ( NULL == (root = (DirStructure*)malloc(sizeof(DirStructure))) ) { fprintf(stderr, "%s: Malloc failed\n", progName); exit(1); } InitializeRootDir(root); if ( -1 == (attr = GetFileAttributes(name)) ) { error = GetLastError(); if (error = ERROR_FILE_NOT_FOUND) { fprintf(stderr, "%s: Cannot find %s\n", progName, name); } else { fprintf(stderr, "%s: Error occurred when getting attributes of %s\n", progName, name); fprintf(stderr, "Error code is %d\n", error); } exit(1); } nameptr = name; last = root; // fix arc file path while(GetFileAttributes(nameptr + pathOffset) == FILE_ATTRIBUTE_DIRECTORY){ nameptr = nameptr + pathOffset; pathOffset = GetFirstDirCount(&nameptr); } while(! END_OF_PATH(nameptr)) { if ( NULL == (item = (DirStructure*)malloc(sizeof(DirStructure))) ) { fprintf(stderr, "%s: Malloc failed\n", progName); exit(1); } // note that since END_OF_PATH(nameptr) = TRUE, next != -1 next = GetFirstDirCount(&nameptr); item->isDir = TRUE; item->next = NULL; item->prev = NULL; item->child = NULL; item->parent = last; strcpy(item->pathName, name); item->pathName[(nameptr - name) + next] = '\0'; strcpy(item->dirName, dirName); strcpy(item->fileName, item->pathName); item->nameOffset = nameptr - name; item->name = item->pathName + item->nameOffset; item->file = NULL; nameptr += next; last->child = item; last = item; } if (! (attr & FILE_ATTRIBUTE_DIRECTORY) ) { last->parent->child = NULL; last->parent->file = last; last->isDir = FALSE; if(stat(name, &sb)) { fprintf(stderr, "%s: %s: Does not exist\n", progName, name); exit(1); } item->filePosition = 0; item->fileNewPosition = 0; item->fileLength = sb.st_size; DiskLastPtr->nextInDisc = item; item->prevInDisc = DiskLastPtr; DiskLastPtr = item; } else { CreateHierarchyFromFilesRecursively(last); } DiskLastPtr->nextInDisc = (DirStructure*)NULL; return root; } void Create(char* currDir, char* name, DirStructure* root) { DirStructure* root2; SetCurrentDirectory(currDir); root2 = CreateHierarchyFromFiles(currDir, name); MergeDirStructure(root, root2); } void CreateArc(char* arcFile, Arg_s* arg, int count) { int i, j; DirStructure rootDir; int first = 1; // save the current directory before changing GetCurrentDirectory(FILENAME_MAX, CurrDir); InitializeRootDir(&rootDir); for (i = 0; i < count; i++) { for (j = 0; j < arg[i].argNum; j++) { Create(arg[i].currDir, (arg[i].argStart)[j], &rootDir); } } SetCurrentDirectory(CurrDir); ConstructFSTFromStructure(arcFile, &rootDir); } void ListArcInDiscOrder(char* name) { DirStructure* item; DirStructure rootDir; DarchHandle* handle; handle = OpenArc(name); ConstructStructureFromFST(handle, &rootDir); if (verbose) { fprintf(stdout, "File start offset: 0x%08x\n\n", handle->fileStart); fprintf(stdout, " Position Length name\n"); fprintf(stdout, "--------------------------------------\n"); } CloseArc(handle); for (item = DiskStart.nextInDisc; item; item = item->nextInDisc) { if (verbose) { fprintf(stdout, " 0x%08x 0x%08x ", item->fileNewPosition, item->fileLength); } fprintf(stdout, "%s\n", item->pathName); } }