1 30 package org.jruby.runtime.builtin.meta; 31 32 import java.io.File ; 33 import java.io.FileInputStream ; 34 import java.io.IOException ; 35 import java.nio.channels.FileChannel ; 36 import java.util.regex.Pattern ; 37 38 import org.jruby.Ruby; 39 import org.jruby.RubyArray; 40 import org.jruby.RubyClass; 41 import org.jruby.RubyDir; 42 import org.jruby.RubyFile; 43 import org.jruby.RubyFileStat; 44 import org.jruby.RubyFileTest; 45 import org.jruby.RubyFixnum; 46 import org.jruby.RubyInteger; 47 import org.jruby.RubyModule; 48 import org.jruby.RubyNumeric; 49 import org.jruby.RubyString; 50 import org.jruby.RubyTime; 51 import org.jruby.runtime.Arity; 52 import org.jruby.runtime.Block; 53 import org.jruby.runtime.ObjectAllocator; 54 import org.jruby.runtime.ThreadContext; 55 import org.jruby.runtime.builtin.IRubyObject; 56 import org.jruby.util.IOModes; 57 import org.jruby.util.JRubyFile; 58 import org.jruby.util.PrintfFormat; 59 import org.jruby.util.collections.SinglyLinkedList; 60 61 public class FileMetaClass extends IOMetaClass { 62 private static final int FNM_NOESCAPE = 1; 63 private static final int FNM_PATHNAME = 2; 64 private static final int FNM_DOTMATCH = 4; 65 private static final int FNM_CASEFOLD = 8; 66 67 public static final PrintfFormat OCTAL_FORMATTER = new PrintfFormat("%o"); 68 69 public FileMetaClass(Ruby runtime) { 70 super("File", RubyFile.class, runtime.getClass("IO"), FILE_ALLOCATOR); 71 } 72 73 public FileMetaClass(String name, RubyClass superClass, ObjectAllocator allocator, SinglyLinkedList parentCRef) { 74 super(name, RubyFile.class, superClass, allocator, parentCRef); 75 } 76 77 protected class FileMeta extends Meta { 78 protected void initializeClass() { 79 Ruby runtime = getRuntime(); 80 RubyString separator = runtime.newString("/"); 81 separator.freeze(); 82 defineConstant("SEPARATOR", separator); 83 defineConstant("Separator", separator); 84 85 RubyString altSeparator = runtime.newString(File.separatorChar == '/' ? "\\" : "/"); 86 altSeparator.freeze(); 87 defineConstant("ALT_SEPARATOR", altSeparator); 88 89 RubyString pathSeparator = runtime.newString(File.pathSeparator); 90 pathSeparator.freeze(); 91 defineConstant("PATH_SEPARATOR", pathSeparator); 92 93 setConstant("BINARY", runtime.newFixnum(IOModes.BINARY)); 97 setConstant("FNM_NOESCAPE", runtime.newFixnum(FNM_NOESCAPE)); 98 setConstant("FNM_CASEFOLD", runtime.newFixnum(FNM_CASEFOLD)); 99 setConstant("FNM_DOTMATCH", runtime.newFixnum(FNM_DOTMATCH)); 100 setConstant("FNM_PATHNAME", runtime.newFixnum(FNM_PATHNAME)); 101 102 setConstant("RDONLY", runtime.newFixnum(IOModes.RDONLY)); 104 setConstant("WRONLY", runtime.newFixnum(IOModes.WRONLY)); 105 setConstant("RDWR", runtime.newFixnum(IOModes.RDWR)); 106 setConstant("CREAT", runtime.newFixnum(IOModes.CREAT)); 107 setConstant("EXCL", runtime.newFixnum(IOModes.EXCL)); 108 setConstant("NOCTTY", runtime.newFixnum(IOModes.NOCTTY)); 109 setConstant("TRUNC", runtime.newFixnum(IOModes.TRUNC)); 110 setConstant("APPEND", runtime.newFixnum(IOModes.APPEND)); 111 setConstant("NONBLOCK", runtime.newFixnum(IOModes.NONBLOCK)); 112 113 setConstant("LOCK_SH", runtime.newFixnum(RubyFile.LOCK_SH)); 115 setConstant("LOCK_EX", runtime.newFixnum(RubyFile.LOCK_EX)); 116 setConstant("LOCK_NB", runtime.newFixnum(RubyFile.LOCK_NB)); 117 setConstant("LOCK_UN", runtime.newFixnum(RubyFile.LOCK_UN)); 118 119 RubyModule constants = defineModuleUnder("Constants"); 121 122 constants.setConstant("BINARY", runtime.newFixnum(32768)); 124 constants.setConstant("FNM_NOESCAPE", runtime.newFixnum(1)); 125 constants.setConstant("FNM_CASEFOLD", runtime.newFixnum(8)); 126 constants.setConstant("FNM_DOTMATCH", runtime.newFixnum(4)); 127 constants.setConstant("FNM_PATHNAME", runtime.newFixnum(2)); 128 129 constants.setConstant("RDONLY", runtime.newFixnum(IOModes.RDONLY)); 131 constants.setConstant("WRONLY", runtime.newFixnum(IOModes.WRONLY)); 132 constants.setConstant("RDWR", runtime.newFixnum(IOModes.RDWR)); 133 constants.setConstant("CREAT", runtime.newFixnum(IOModes.CREAT)); 134 constants.setConstant("EXCL", runtime.newFixnum(IOModes.EXCL)); 135 constants.setConstant("NOCTTY", runtime.newFixnum(IOModes.NOCTTY)); 136 constants.setConstant("TRUNC", runtime.newFixnum(IOModes.TRUNC)); 137 constants.setConstant("APPEND", runtime.newFixnum(IOModes.APPEND)); 138 constants.setConstant("NONBLOCK", runtime.newFixnum(IOModes.NONBLOCK)); 139 140 constants.setConstant("LOCK_SH", runtime.newFixnum(RubyFile.LOCK_SH)); 142 constants.setConstant("LOCK_EX", runtime.newFixnum(RubyFile.LOCK_EX)); 143 constants.setConstant("LOCK_NB", runtime.newFixnum(RubyFile.LOCK_NB)); 144 constants.setConstant("LOCK_UN", runtime.newFixnum(RubyFile.LOCK_UN)); 145 146 152 extendObject(runtime.getModule("FileTest")); 153 154 defineFastSingletonMethod("basename", Arity.optional()); 155 defineFastSingletonMethod("chmod", Arity.required(2)); 156 defineFastSingletonMethod("chown", Arity.required(2)); 157 defineFastSingletonMethod("delete", Arity.optional(), "unlink"); 158 defineFastSingletonMethod("dirname", Arity.singleArgument()); 159 defineFastSingletonMethod("expand_path", Arity.optional()); 160 defineFastSingletonMethod("extname", Arity.singleArgument()); 161 defineFastSingletonMethod("fnmatch", Arity.optional()); 162 defineFastSingletonMethod("fnmatch?", Arity.optional(), "fnmatch"); 163 defineFastSingletonMethod("join", Arity.optional()); 164 defineFastSingletonMethod("lstat", Arity.singleArgument()); 165 defineFastSingletonMethod("mtime", Arity.singleArgument()); 166 defineFastSingletonMethod("ctime", Arity.singleArgument()); 167 defineSingletonMethod("open", Arity.optional()); 168 defineFastSingletonMethod("rename", Arity.twoArguments()); 169 defineFastSingletonMethod("size?", Arity.singleArgument(), "size_p"); 170 defineFastSingletonMethod("split", Arity.singleArgument()); 171 defineFastSingletonMethod("stat", Arity.singleArgument(), "lstat"); 172 defineFastSingletonMethod("symlink?", Arity.singleArgument(), "symlink_p"); 173 defineFastSingletonMethod("truncate", Arity.twoArguments()); 174 defineFastSingletonMethod("utime", Arity.optional()); 175 defineFastSingletonMethod("unlink", Arity.optional()); 176 177 defineFastMethod("chmod", Arity.required(1)); 180 defineFastMethod("chown", Arity.required(1)); 181 defineFastMethod("ctime", Arity.noArguments()); 182 defineMethod("initialize", Arity.optional()); 183 defineFastMethod("path", Arity.noArguments()); 184 defineFastMethod("stat", Arity.noArguments()); 185 defineFastMethod("truncate", Arity.singleArgument()); 186 defineFastMethod("flock", Arity.singleArgument()); 187 188 RubyFileStat.createFileStatClass(runtime); 189 } 190 }; 191 192 protected Meta getMeta() { 193 return new FileMeta(); 194 } 195 196 public RubyClass newSubClass(String name, SinglyLinkedList parentCRef) { 197 return new FileMetaClass(name, this, FILE_ALLOCATOR, parentCRef); 198 } 199 200 private static ObjectAllocator FILE_ALLOCATOR = new ObjectAllocator() { 201 public IRubyObject allocate(Ruby runtime, RubyClass klass) { 202 RubyFile instance = new RubyFile(runtime, klass); 203 204 instance.setMetaClass(klass); 205 206 return instance; 207 } 208 }; 209 210 public IRubyObject basename(IRubyObject[] args) { 211 checkArgumentCount(args, 1, 2); 212 213 String name = RubyString.stringValue(args[0]).toString(); 214 if (name.length() > 1 && name.charAt(name.length() - 1) == '/') { 215 name = name.substring(0, name.length() - 1); 216 } 217 218 int slashCount = 0; 220 int length = name.length(); 221 for (int i = length - 1; i >= 0; i--) { 222 char c = name.charAt(i); 223 if (c != '/' && c != '\\') { 224 break; 225 } 226 slashCount++; 227 } 228 if (slashCount > 0 && length > 1) { 229 name = name.substring(0, name.length() - slashCount); 230 } 231 232 int index = name.lastIndexOf('/'); 233 if (index == -1) { 234 index = name.lastIndexOf('\\'); 236 } 237 238 if (!name.equals("/") && index != -1) { 239 name = name.substring(index + 1); 240 } 241 242 if (args.length == 2) { 243 String ext = RubyString.stringValue(args[1]).toString(); 244 if (".*".equals(ext)) { 245 index = name.lastIndexOf('.'); 246 if (index > 0) { name = name.substring(0, index); 248 } 249 } else if (name.endsWith(ext)) { 250 name = name.substring(0, name.length() - ext.length()); 251 } 252 } 253 return getRuntime().newString(name).infectBy(args[0]); 254 } 255 256 public IRubyObject chmod(IRubyObject[] args) { 257 checkArgumentCount(args, 2, -1); 258 259 int count = 0; 260 RubyInteger mode = args[0].convertToInteger(); 261 for (int i = 1; i < args.length; i++) { 262 IRubyObject filename = args[i]; 263 264 if (!RubyFileTest.exist_p(filename, filename.convertToString()).isTrue()) { 265 throw getRuntime().newErrnoENOENTError("No such file or directory - " + filename); 266 } 267 268 try { 269 Process chmod = Runtime.getRuntime().exec("chmod " + OCTAL_FORMATTER.sprintf(mode.getLongValue()) + " " + filename); 270 chmod.waitFor(); 271 int result = chmod.exitValue(); 272 if (result == 0) { 273 count++; 274 } 275 } catch (IOException ioe) { 276 } catch (InterruptedException ie) { 278 } 280 } 281 282 return getRuntime().newFixnum(count); 283 } 284 285 public IRubyObject chown(IRubyObject[] args) { 286 checkArgumentCount(args, 2, -1); 287 288 int count = 0; 289 RubyInteger owner = args[0].convertToInteger(); 290 for (int i = 1; i < args.length; i++) { 291 IRubyObject filename = args[i]; 292 293 if (!RubyFileTest.exist_p(filename, filename.convertToString()).isTrue()) { 294 throw getRuntime().newErrnoENOENTError("No such file or directory - " + filename); 295 } 296 297 try { 298 Process chown = Runtime.getRuntime().exec("chown " + owner + " " + filename); 299 chown.waitFor(); 300 int result = chown.exitValue(); 301 if (result == 0) { 302 count++; 303 } 304 } catch (IOException ioe) { 305 } catch (InterruptedException ie) { 307 } 309 } 310 311 return getRuntime().newFixnum(count); 312 } 313 314 public IRubyObject dirname(IRubyObject arg) { 315 RubyString filename = RubyString.stringValue(arg); 316 String name = filename.toString().replace('\\', '/'); 317 if (name.length() > 1 && name.charAt(name.length() - 1) == '/') { 318 name = name.substring(0, name.length() - 1); 319 } 320 int index = name.lastIndexOf('/'); 322 if (index == -1) { 323 return getRuntime().newString("."); 324 } 325 if (index == 0) { 326 return getRuntime().newString("/"); 327 } 328 return getRuntime().newString(name.substring(0, index)).infectBy(filename); 329 } 330 331 public IRubyObject extname(IRubyObject arg) { 332 RubyString filename = RubyString.stringValue(arg); 333 334 String name = filename.toString(); 335 336 int index = name.lastIndexOf('/'); 339 if (index == -1) { 340 index = name.lastIndexOf('\\'); 342 } 343 name = name.substring(index + 1); 344 345 int ix = name.lastIndexOf("."); 346 if(ix == -1) { 347 return getRuntime().newString(""); 348 } else { 349 return getRuntime().newString(name.substring(ix)); 350 } 351 } 352 353 public IRubyObject expand_path(IRubyObject[] args) { 354 checkArgumentCount(args, 1, 2); 355 String relativePath = RubyString.stringValue(args[0]).toString(); 356 int pathLength = relativePath.length(); 357 358 if (pathLength >= 1 && relativePath.charAt(0) == '~') { 359 int userEnd = relativePath.indexOf('/'); 361 362 if (userEnd == -1) { 363 if (pathLength == 1) { 364 relativePath = RubyDir.getHomeDirectoryPath(this).toString(); 366 } else { 367 userEnd = pathLength; 369 } 370 } 371 372 if (userEnd == 1) { 373 relativePath = RubyDir.getHomeDirectoryPath(this).toString() + 375 relativePath.substring(1); 376 } else if (userEnd > 1){ 377 String user = relativePath.substring(1, userEnd); 379 IRubyObject dir = RubyDir.getHomeDirectoryPath(this, user); 380 381 if (dir.isNil()) { 382 throw getRuntime().newArgumentError("user " + user + " does not exist"); 383 } 384 385 relativePath = "" + dir + 386 (pathLength == userEnd ? "" : relativePath.substring(userEnd)); 387 } 388 } 389 390 if (new File (relativePath).isAbsolute()) { 391 try { 392 return getRuntime().newString(JRubyFile.create(relativePath, "").getCanonicalPath()); 393 } catch(IOException e) { 394 return getRuntime().newString(relativePath); 395 } 396 } 397 398 String cwd = getRuntime().getCurrentDirectory(); 399 if (args.length == 2 && !args[1].isNil()) { 400 cwd = RubyString.stringValue(args[1]).toString(); 401 } 402 403 if (cwd == null) { 405 return getRuntime().getNil(); 406 } 407 408 JRubyFile path = JRubyFile.create(cwd, relativePath); 409 410 String extractedPath; 411 try { 412 extractedPath = path.getCanonicalPath(); 413 } catch (IOException e) { 414 extractedPath = path.getAbsolutePath(); 415 } 416 return getRuntime().newString(extractedPath); 417 } 418 419 428 public IRubyObject fnmatch(IRubyObject[] args) { 430 checkArgumentCount(args, 2, -1); 431 String pattern = args[0].convertToString().toString(); 432 RubyString path = args[1].convertToString(); 433 int opts = (int) (args.length > 2 ? args[2].convertToInteger().getLongValue() : 0); 434 435 boolean dot = pattern.startsWith("."); 436 437 pattern = pattern.replaceAll("(\\.)", "\\\\$1"); 438 pattern = pattern.replaceAll("(?<=[^\\\\])\\*", ".*"); 439 pattern = pattern.replaceAll("^\\*", ".*"); 440 pattern = pattern.replaceAll("(?<=[^\\\\])\\?", "."); 441 pattern = pattern.replaceAll("^\\?", "."); 442 if ((opts & FNM_NOESCAPE) != FNM_NOESCAPE) { 443 pattern = pattern.replaceAll("\\\\([^\\\\*\\\\?])", "$1"); 444 } 445 pattern = pattern.replaceAll("\\{", "\\\\{"); 446 pattern = pattern.replaceAll("\\}", "\\\\}"); 447 pattern = "^" + pattern + "$"; 448 449 if (path.toString().startsWith(".") && !dot) { 450 return getRuntime().newBoolean(false); 451 } 452 453 return getRuntime().newBoolean(Pattern.matches(pattern, path.toString())); 454 } 455 456 460 public RubyString join(IRubyObject[] args) { 461 boolean isTainted = false; 462 StringBuffer buffer = new StringBuffer (); 463 464 for (int i = 0; i < args.length; i++) { 465 if (args[i].isTaint()) { 466 isTainted = true; 467 } 468 String element; 469 if (args[i] instanceof RubyString) { 470 element = args[i].toString(); 471 } else if (args[i] instanceof RubyArray) { 472 element = join(((RubyArray) args[i]).toJavaArray()).toString(); 474 } else { 475 element = args[i].convertToString().toString(); 476 } 477 478 chomp(buffer); 479 if (i > 0 && !element.startsWith("/") && !element.startsWith("\\")) { 480 buffer.append("/"); 481 } 482 buffer.append(element); 483 } 484 485 RubyString fixedStr = RubyString.newString(getRuntime(), buffer.toString()); 486 fixedStr.setTaint(isTainted); 487 return fixedStr; 488 } 489 490 private void chomp(StringBuffer buffer) { 491 int lastIndex = buffer.length() - 1; 492 493 while (lastIndex >= 0 && (buffer.lastIndexOf("/") == lastIndex || buffer.lastIndexOf("\\") == lastIndex)) { 494 buffer.setLength(lastIndex); 495 lastIndex--; 496 } 497 } 498 499 public IRubyObject lstat(IRubyObject filename) { 500 RubyString name = RubyString.stringValue(filename); 501 return getRuntime().newRubyFileStat(name.toString()); 502 } 503 504 public IRubyObject ctime(IRubyObject filename) { 505 RubyString name = RubyString.stringValue(filename); 506 return getRuntime().newTime(JRubyFile.create(getRuntime().getCurrentDirectory(),name.toString()).getParentFile().lastModified()); 507 } 508 509 public IRubyObject mtime(IRubyObject filename) { 510 RubyString name = RubyString.stringValue(filename); 511 512 return getRuntime().newTime(JRubyFile.create(getRuntime().getCurrentDirectory(),name.toString()).lastModified()); 513 } 514 515 public IRubyObject open(IRubyObject[] args, Block block) { 516 return open(args, true, block); 517 } 518 519 public IRubyObject open(IRubyObject[] args, boolean tryToYield, Block block) { 520 checkArgumentCount(args, 1, -1); 521 Ruby runtime = getRuntime(); 522 ThreadContext tc = runtime.getCurrentContext(); 523 524 RubyString pathString = RubyString.stringValue(args[0]); 525 pathString.checkSafeString(); 526 String path = pathString.toString(); 527 528 IOModes modes = 529 args.length >= 2 ? getModes(args[1]) : new IOModes(runtime, IOModes.RDONLY); 530 RubyFile file = new RubyFile(runtime, this); 531 532 RubyInteger fileMode = 533 args.length >= 3 ? args[2].convertToInteger() : null; 534 535 file.openInternal(path, modes); 536 537 if (fileMode != null) { 538 chmod(new IRubyObject[] {fileMode, pathString}); 539 } 540 541 if (tryToYield && block.isGiven()) { 542 try { 543 return tc.yield(file, block); 544 } finally { 545 file.close(); 546 } 547 } 548 549 return file; 550 } 551 552 public IRubyObject rename(IRubyObject oldName, IRubyObject newName) { 553 RubyString oldNameString = RubyString.stringValue(oldName); 554 RubyString newNameString = RubyString.stringValue(newName); 555 oldNameString.checkSafeString(); 556 newNameString.checkSafeString(); 557 JRubyFile oldFile = JRubyFile.create(getRuntime().getCurrentDirectory(),oldNameString.toString()); 558 JRubyFile newFile = JRubyFile.create(getRuntime().getCurrentDirectory(),newNameString.toString()); 559 560 if (!oldFile.exists() || !newFile.getParentFile().exists()) { 561 throw getRuntime().newErrnoENOENTError("No such file or directory - " + oldNameString + " or " + newNameString); 562 } 563 oldFile.renameTo(JRubyFile.create(getRuntime().getCurrentDirectory(),newNameString.toString())); 564 565 return RubyFixnum.zero(getRuntime()); 566 } 567 568 public IRubyObject size_p(IRubyObject filename) { 569 long size = 0; 570 571 try { 572 FileInputStream fis = new FileInputStream (new File (filename.toString())); 573 FileChannel chan = fis.getChannel(); 574 size = chan.size(); 575 chan.close(); 576 fis.close(); 577 } catch (IOException ioe) { 578 } 580 581 if (size == 0) { 582 return getRuntime().getNil(); 583 } 584 585 return getRuntime().newFixnum(size); 586 } 587 588 public RubyArray split(IRubyObject arg) { 589 RubyString filename = RubyString.stringValue(arg); 590 591 return filename.getRuntime().newArray(dirname(filename), 592 basename(new IRubyObject[] { filename })); 593 } 594 595 public IRubyObject symlink_p(IRubyObject arg1) { 596 RubyString filename = RubyString.stringValue(arg1); 597 598 JRubyFile file = JRubyFile.create(getRuntime().getCurrentDirectory(), filename.toString()); 599 600 try { 601 File absoluteParent = file.getAbsoluteFile().getParentFile(); 604 File canonicalParent = file.getAbsoluteFile().getParentFile().getCanonicalFile(); 605 606 if (canonicalParent.getAbsolutePath().equals(absoluteParent.getAbsolutePath())) { 607 return file.getAbsolutePath().equals(file.getCanonicalPath()) ? getRuntime().getFalse() : getRuntime().getTrue(); 609 } 610 611 file = JRubyFile.create(getRuntime().getCurrentDirectory(), canonicalParent.getAbsolutePath() + "/" + file.getName()); 613 return file.getAbsolutePath().equals(file.getCanonicalPath()) ? getRuntime().getFalse() : getRuntime().getTrue(); 614 } catch (IOException ioe) { 615 return getRuntime().getFalse(); 617 } 618 } 619 620 public IRubyObject truncate(IRubyObject arg1, IRubyObject arg2) { 622 RubyString filename = RubyString.stringValue(arg1); 623 RubyFixnum newLength = (RubyFixnum) arg2.convertToType("Fixnum", "to_int", true); 624 IRubyObject[] args = new IRubyObject[] { filename, getRuntime().newString("w+") }; 625 RubyFile file = (RubyFile) open(args, false, null); 626 file.truncate(newLength); 627 file.close(); 628 629 return RubyFixnum.zero(getRuntime()); 630 } 631 632 635 public IRubyObject utime(IRubyObject[] args) { 636 checkArgumentCount(args, 2, -1); 637 638 640 long mtime; 641 if (args[1] instanceof RubyTime) { 642 mtime = ((RubyTime) args[1]).getJavaDate().getTime(); 643 } else if (args[1] instanceof RubyNumeric) { 644 mtime = RubyNumeric.num2long(args[1]); 645 } else { 646 mtime = 0; 647 } 648 649 for (int i = 2, j = args.length; i < j; i++) { 650 RubyString filename = RubyString.stringValue(args[i]); 651 filename.checkSafeString(); 652 JRubyFile fileToTouch = JRubyFile.create(getRuntime().getCurrentDirectory(),filename.toString()); 653 654 if (!fileToTouch.exists()) { 655 throw getRuntime().newErrnoENOENTError(" No such file or directory - \"" + 656 filename + "\""); 657 } 658 659 fileToTouch.setLastModified(mtime); 660 } 661 662 return getRuntime().newFixnum(args.length - 2); 663 } 664 665 public IRubyObject unlink(IRubyObject[] args) { 666 for (int i = 0; i < args.length; i++) { 667 RubyString filename = RubyString.stringValue(args[i]); 668 filename.checkSafeString(); 669 JRubyFile lToDelete = JRubyFile.create(getRuntime().getCurrentDirectory(),filename.toString()); 670 if (!lToDelete.exists()) { 671 throw getRuntime().newErrnoENOENTError(" No such file or directory - \"" + filename + "\""); 672 } 673 if (!lToDelete.delete()) { 674 return getRuntime().getFalse(); 675 } 676 } 677 return getRuntime().newFixnum(args.length); 678 } 679 680 private IOModes getModes(IRubyObject object) { 682 if (object instanceof RubyString) { 683 return new IOModes(getRuntime(), ((RubyString)object).toString()); 684 } else if (object instanceof RubyFixnum) { 685 return new IOModes(getRuntime(), ((RubyFixnum)object).getLongValue()); 686 } 687 688 throw getRuntime().newTypeError("Invalid type for modes"); 689 } 690 691 692 } 693 | Popular Tags |