1 /*
   2  * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 package org.graalvm.compiler.debug;
  24 
  25 import java.io.IOException;
  26 import java.nio.file.InvalidPathException;
  27 import java.nio.file.Path;
  28 import java.nio.file.Paths;
  29 import java.util.HashMap;
  30 import java.util.concurrent.atomic.AtomicInteger;
  31 import java.util.concurrent.atomic.AtomicLong;
  32 
  33 import org.graalvm.compiler.options.OptionKey;
  34 import org.graalvm.compiler.options.OptionValues;
  35 
  36 /**
  37  * Miscellaneous methods for modifying and generating file system paths.
  38  */
  39 public class PathUtilities {
  40 
  41     private static final AtomicLong globalTimeStamp = new AtomicLong();
  42     /**
  43      * This generates a per thread persistent id to aid mapping related dump files with each other.
  44      */
  45     private static final ThreadLocal<PerThreadSequence> threadDumpId = new ThreadLocal<>();
  46     private static final AtomicInteger dumpId = new AtomicInteger();
  47 
  48     static class PerThreadSequence {
  49         final int threadID;
  50         HashMap<String, Integer> sequences = new HashMap<>(2);
  51 
  52         PerThreadSequence(int threadID) {
  53             this.threadID = threadID;
  54         }
  55 
  56         String generateID(String extension) {
  57             Integer box = sequences.get(extension);
  58             if (box == null) {
  59                 sequences.put(extension, 1);
  60                 return Integer.toString(threadID);
  61             } else {
  62                 sequences.put(extension, box + 1);
  63                 return Integer.toString(threadID) + '-' + box;
  64             }
  65         }
  66     }
  67 
  68     private static String getThreadDumpId(String extension) {
  69         PerThreadSequence id = threadDumpId.get();
  70         if (id == null) {
  71             id = new PerThreadSequence(dumpId.incrementAndGet());
  72             threadDumpId.set(id);
  73         }
  74         return id.generateID(extension);
  75     }
  76 
  77     /**
  78      * Prepends a period (i.e., {@code '.'}) to an non-null, non-empty string representation a file
  79      * extension if the string does not already start with a period.
  80      *
  81      * @return {@code ext} unmodified if it is null, empty or already starts with a period other
  82      *         {@code "." + ext}
  83      */
  84     public static String formatExtension(String ext) {
  85         if (ext == null || ext.length() == 0) {
  86             return "";
  87         }
  88         return "." + ext;
  89     }
  90 
  91     /**
  92      * Gets a time stamp for the current process. This method will always return the same value for
  93      * the current VM execution.
  94      */
  95     public static long getGlobalTimeStamp() {
  96         if (globalTimeStamp.get() == 0) {
  97             globalTimeStamp.compareAndSet(0, System.currentTimeMillis());
  98         }
  99         return globalTimeStamp.get();
 100     }
 101 
 102     /**
 103      * Generates a {@link Path} using the format "%s-%d_%d%s" with the {@code baseNameOption}, a
 104      * {@link #getGlobalTimeStamp() global timestamp} , {@link #getThreadDumpId a per thread unique
 105      * id} and an optional {@code extension}.
 106      *
 107      * @return the output file path or null if the flag is null
 108      */
 109     public static Path getPath(OptionValues options, OptionKey<String> baseNameOption, String extension) throws IOException {
 110         return getPath(options, baseNameOption, extension, true);
 111     }
 112 
 113     /**
 114      * Generate a {@link Path} using the format "%s-%d_%s" with the {@code baseNameOption}, a
 115      * {@link #getGlobalTimeStamp() global timestamp} and an optional {@code extension} .
 116      *
 117      * @return the output file path or null if the flag is null
 118      */
 119     public static Path getPathGlobal(OptionValues options, OptionKey<String> baseNameOption, String extension) throws IOException {
 120         return getPath(options, baseNameOption, extension, false);
 121     }
 122 
 123     private static Path getPath(OptionValues options, OptionKey<String> baseNameOption, String extension, boolean includeThreadId) throws IOException {
 124         if (baseNameOption.getValue(options) == null) {
 125             return null;
 126         }
 127         String ext = formatExtension(extension);
 128         final String name = includeThreadId
 129                         ? String.format("%s-%d_%s%s", baseNameOption.getValue(options), getGlobalTimeStamp(), getThreadDumpId(ext), ext)
 130                         : String.format("%s-%d%s", baseNameOption.getValue(options), getGlobalTimeStamp(), ext);
 131         Path result = Paths.get(name);
 132         if (result.isAbsolute()) {
 133             return result;
 134         }
 135         Path dumpDir = DebugOptions.getDumpDirectory(options);
 136         return dumpDir.resolve(name).normalize();
 137     }
 138 
 139     /**
 140      * Gets a value based on {@code name} that can be passed to {@link Paths#get(String, String...)}
 141      * without causing an {@link InvalidPathException}.
 142      *
 143      * @return {@code name} with all characters invalid for the current file system replaced by
 144      *         {@code '_'}
 145      */
 146     public static String sanitizeFileName(String name) {
 147         try {
 148             Paths.get(name);
 149             return name;
 150         } catch (InvalidPathException e) {
 151             // fall through
 152         }
 153         StringBuilder buf = new StringBuilder(name.length());
 154         for (int i = 0; i < name.length(); i++) {
 155             char c = name.charAt(i);
 156             try {
 157                 Paths.get(String.valueOf(c));
 158             } catch (InvalidPathException e) {
 159                 buf.append('_');
 160             }
 161             buf.append(c);
 162         }
 163         return buf.toString();
 164     }
 165 }