| 1 | token.txt | |
| 1 | 2 |
| 44 | 44 | |
| 45 | 45 | # Archives |
| 46 | ADD "https://www.omnibus-type.com/wp-content/uploads/Archivo-Narrow.zip" "archivo-narrow.zip" | |
| 46 | 47 | ADD "https://fonts.google.com/download?family=Courier%20Prime" "courier-prime.zip" |
| 47 | 48 | ADD "https://fonts.google.com/download?family=Inconsolata" "inconsolata.zip" |
| 48 | 49 | ADD "https://fonts.google.com/download?family=Libre%20Baskerville" "libre-baskerville.zip" |
| 50 | ADD "https://fonts.google.com/download?family=Niconne" "niconne.zip" | |
| 49 | 51 | ADD "https://fonts.google.com/download?family=Nunito" "nunito.zip" |
| 50 | 52 | ADD "https://fonts.google.com/download?family=Roboto" "roboto.zip" |
| 51 | 53 | ADD "https://fonts.google.com/download?family=Roboto%20Mono" "roboto-mono.zip" |
| 52 | 54 | ADD "https://github.com/adobe-fonts/source-serif/releases/download/4.004R/source-serif-4.004.zip" "source-serif.zip" |
| 53 | ADD "https://www.omnibus-type.com/wp-content/uploads/Archivo-Narrow.zip" "archivo-narrow.zip" | |
| 54 | 55 | |
| 55 | 56 | # Typesetting software |
| ... | ||
| 74 | 75 | echo "PS1='\\u@typesetter:\\w\\$ '" >> $PROFILE && \ |
| 75 | 76 | unzip -d $CONTEXT_HOME $DOWNLOAD_DIR/context.zip && \ |
| 77 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/archivo-narrow.zip "Archivo-Narrow/otf/*.otf" && \ | |
| 76 | 78 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/courier-prime.zip "*.ttf" && \ |
| 77 | 79 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/libre-baskerville.zip "*.ttf" && \ |
| 78 | 80 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/inconsolata.zip "**/Inconsolata/*.ttf" && \ |
| 81 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/niconne.zip "*.ttf" && \ | |
| 79 | 82 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/nunito.zip "static/*.ttf" && \ |
| 80 | 83 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/roboto.zip "*.ttf" && \ |
| 81 | 84 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/roboto-mono.zip "static/*.ttf" && \ |
| 82 | 85 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/source-serif.zip "source-serif-4.004/OTF/SourceSerif4-*.otf" && \ |
| 83 | unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/archivo-narrow.zip "Archivo-Narrow/otf/*.otf" && \ | |
| 84 | 86 | mv $DOWNLOAD_DIR/*tf $FONTS_DIR && \ |
| 85 | 87 | fc-cache -f -v && \ |
| 137 | 137 | # --------------------------------------------------------------------------- |
| 138 | 138 | get_mountpoint() { |
| 139 | $log "Mounting ${1} as ${2}" | |
| 140 | ||
| 141 | 139 | local result="" |
| 142 | 140 | local binding="ro" |
| ... | ||
| 179 | 177 | declare -r mount_images=$(get_mountpoint_images) |
| 180 | 178 | declare -r mount_fonts=$(get_mountpoint_fonts) |
| 179 | ||
| 180 | $log "mount_source = '${mount_source}'" | |
| 181 | $log "mount_target = '${mount_target}'" | |
| 182 | $log "mount_images = '${mount_images}'" | |
| 183 | $log "mount_fonts = '${mount_fonts}'" | |
| 181 | 184 | |
| 182 | 185 | ${CONTAINER_EXE} run \ |
| 2 | 2 | package com.keenwrite.events; |
| 3 | 3 | |
| 4 | import com.keenwrite.AppCommands; | |
| 5 | ||
| 6 | 4 | import java.util.List; |
| 7 | 5 | |
| ... | ||
| 18 | 16 | */ |
| 19 | 17 | public final class StatusEvent implements AppEvent { |
| 20 | /** | |
| 21 | * Reference a class in the top-level package that doesn't depend on any | |
| 22 | * JavaFX APIs. | |
| 23 | */ | |
| 24 | private static final String PACKAGE_NAME = AppCommands.class.getPackageName(); | |
| 25 | ||
| 26 | 18 | private static final String ENGLISHIFY = |
| 27 | 19 | "(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])"; |
| ... | ||
| 47 | 39 | } |
| 48 | 40 | |
| 41 | /** | |
| 42 | * Constructs a new event that contains information about an unexpected issue. | |
| 43 | * | |
| 44 | * @param problem The issue encountered by the software, never {@code null}. | |
| 45 | */ | |
| 49 | 46 | public StatusEvent( final Throwable problem ) { |
| 50 | this( "", problem ); | |
| 47 | this( problem.getMessage(), problem ); | |
| 51 | 48 | } |
| 52 | 49 | |
| 53 | 50 | /** |
| 54 | 51 | * @param message The human-readable message text. |
| 55 | 52 | * @param problem May be {@code null} if no exception was thrown. |
| 56 | 53 | */ |
| 57 | 54 | public StatusEvent( final String message, final Throwable problem ) { |
| 58 | assert message != null; | |
| 59 | mMessage = message; | |
| 55 | mMessage = message == null ? "" : message; | |
| 60 | 56 | mProblem = problem; |
| 61 | 57 | } |
| ... | ||
| 75 | 71 | stream( trace.getStackTrace() ) |
| 76 | 72 | .takeWhile( StatusEvent::filter ) |
| 77 | .limit( 10 ) | |
| 73 | .limit( 15 ) | |
| 78 | 74 | .toList() |
| 79 | 75 | .forEach( e -> sb.append( e.toString() ).append( NEWLINE ) ); |
| ... | ||
| 102 | 98 | private static boolean filter( final StackTraceElement e ) { |
| 103 | 99 | final var clazz = e.getClassName(); |
| 104 | return !(clazz.contains( PACKAGE_NAME ) || | |
| 105 | clazz.contains( "org.renjin." ) || | |
| 100 | return !(clazz.contains( "org.renjin." ) || | |
| 106 | 101 | clazz.contains( "sun." ) || |
| 107 | 102 | clazz.contains( "flexmark." ) || |
| 108 | clazz.contains( "java." )); | |
| 103 | clazz.contains( "java." ) | |
| 104 | ); | |
| 109 | 105 | } |
| 110 | 106 | |
| 2 | 2 | package com.keenwrite.io; |
| 3 | 3 | |
| 4 | import java.io.File; | |
| 5 | 4 | import java.io.FileNotFoundException; |
| 6 | 5 | |
| ... | ||
| 17 | 16 | public CommandNotFoundException( final String command ) { |
| 18 | 17 | super( command ); |
| 19 | } | |
| 20 | ||
| 21 | /** | |
| 22 | * Creates a new exception indicating that the given command could not be | |
| 23 | * found (or executed). | |
| 24 | * | |
| 25 | * @param file The binary file's command name that could not be run. | |
| 26 | */ | |
| 27 | public CommandNotFoundException( final File file ) { | |
| 28 | this( file.getAbsolutePath() ); | |
| 29 | 18 | } |
| 30 | 19 | } |
| 2 | 2 | package com.keenwrite.io; |
| 3 | 3 | |
| 4 | import org.jetbrains.annotations.NotNull; | |
| 5 | ||
| 4 | 6 | import java.io.File; |
| 5 | 7 | import java.io.FileInputStream; |
| 6 | 8 | import java.io.IOException; |
| 7 | 9 | import java.nio.file.Path; |
| 8 | 10 | import java.security.MessageDigest; |
| 9 | 11 | import java.security.NoSuchAlgorithmException; |
| 12 | import java.util.ArrayList; | |
| 10 | 13 | import java.util.Optional; |
| 11 | 14 | import java.util.function.Function; |
| 12 | import java.util.regex.Pattern; | |
| 15 | import java.util.function.Predicate; | |
| 13 | 16 | |
| 17 | import static com.keenwrite.constants.Constants.USER_DIRECTORY; | |
| 18 | import static com.keenwrite.events.StatusEvent.clue; | |
| 19 | import static com.keenwrite.io.WindowsRegistry.pathsWindows; | |
| 14 | 20 | import static com.keenwrite.util.DataTypeConverter.toHex; |
| 15 | 21 | import static java.lang.System.getenv; |
| 16 | 22 | import static java.nio.file.Files.isExecutable; |
| 17 | import static java.util.regex.Pattern.compile; | |
| 18 | 23 | import static java.util.regex.Pattern.quote; |
| 19 | 24 | import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS; |
| ... | ||
| 29 | 34 | private static final String[] EXTENSIONS = new String[] |
| 30 | 35 | {"", ".exe", ".bat", ".cmd", ".msi", ".com"}; |
| 36 | ||
| 37 | private static final String WHERE_COMMAND = | |
| 38 | IS_OS_WINDOWS ? "where" : "which"; | |
| 31 | 39 | |
| 32 | 40 | /** |
| 33 | 41 | * Number of bytes to read at a time when computing this file's checksum. |
| 34 | 42 | */ |
| 35 | 43 | private static final int BUFFER_SIZE = 16384; |
| 36 | ||
| 37 | //@formatter:off | |
| 38 | private static final String SYS_KEY = | |
| 39 | "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"; | |
| 40 | private static final String USR_KEY = | |
| 41 | "HKEY_CURRENT_USER\\Environment"; | |
| 42 | //@formatter:on | |
| 43 | ||
| 44 | /** | |
| 45 | * Regular expression pattern for matching %VARIABLE% names. | |
| 46 | */ | |
| 47 | private static final String VAR_REGEX = "%.*?%"; | |
| 48 | private static final Pattern VAR_PATTERN = compile( VAR_REGEX ); | |
| 49 | ||
| 50 | private static final String REG_REGEX = "\\s*path\\s+REG_EXPAND_SZ\\s+(.*)"; | |
| 51 | private static final Pattern REG_PATTERN = compile( REG_REGEX ); | |
| 52 | 44 | |
| 53 | 45 | /** |
| ... | ||
| 73 | 65 | |
| 74 | 66 | /** |
| 75 | * Answers whether the path returned from {@link #locate()} is an executable | |
| 76 | * that can be run using a {@link ProcessBuilder}. | |
| 67 | * Answers whether an executable can be found that can be run using a | |
| 68 | * {@link ProcessBuilder}. | |
| 69 | * | |
| 70 | * @return {@code true} if the executable is runnable. | |
| 77 | 71 | */ |
| 78 | 72 | public boolean canRun() { |
| ... | ||
| 93 | 87 | * the fully qualified path, otherwise an empty result. |
| 94 | 88 | * |
| 95 | * @param map The mapping function of registry variable names to values. | |
| 96 | * @return The fully qualified {@link Path} to the executable filename | |
| 97 | * provided at construction time. | |
| 89 | * @return Fully qualified path to the executable, if found. | |
| 98 | 90 | */ |
| 99 | public Optional<Path> locate( final Function<String, String> map ) { | |
| 91 | public Optional<Path> locate() { | |
| 92 | final var dirList = new ArrayList<String>(); | |
| 93 | final var paths = pathsSane(); | |
| 94 | int began = 0; | |
| 95 | int ended; | |
| 96 | ||
| 97 | while( (ended = paths.indexOf( pathSeparatorChar, began )) != -1 ) { | |
| 98 | final var dir = paths.substring( began, ended ); | |
| 99 | began = ended + 1; | |
| 100 | ||
| 101 | dirList.add( dir ); | |
| 102 | } | |
| 103 | ||
| 104 | final var dirs = dirList.toArray( new String[]{} ); | |
| 105 | var path = locate( dirs, "Wizard.container.executable.path" ); | |
| 106 | ||
| 107 | if( path.isEmpty() ) { | |
| 108 | clue(); | |
| 109 | ||
| 110 | try { | |
| 111 | path = where(); | |
| 112 | } catch( final IOException ex ) { | |
| 113 | clue( "Wizard.container.executable.which", ex ); | |
| 114 | } | |
| 115 | } | |
| 116 | ||
| 117 | return path.isPresent() | |
| 118 | ? path | |
| 119 | : locate( System::getenv, | |
| 120 | IS_OS_WINDOWS | |
| 121 | ? "Wizard.container.executable.registry" | |
| 122 | : "Wizard.container.executable.path" ); | |
| 123 | } | |
| 124 | ||
| 125 | private Optional<Path> locate( final String[] dirs, final String msg ) { | |
| 100 | 126 | final var exe = getName(); |
| 101 | final var paths = paths( map ).split( quote( pathSeparator ) ); | |
| 102 | 127 | |
| 103 | for( final var path : paths ) { | |
| 104 | final var p = Path.of( path ).resolve( exe ); | |
| 128 | for( final var dir : dirs ) { | |
| 129 | final var p = Path.of( dir ).resolve( exe ); | |
| 105 | 130 | |
| 106 | 131 | for( final var extension : EXTENSIONS ) { |
| ... | ||
| 113 | 138 | } |
| 114 | 139 | |
| 140 | clue( msg ); | |
| 115 | 141 | return Optional.empty(); |
| 116 | 142 | } |
| 117 | 143 | |
| 118 | /** | |
| 119 | * Convenience method that locates a binary executable file in the path | |
| 120 | * by using {@link System#getenv(String)} to retrieve environment variables | |
| 121 | * that are expanded when parsing the PATH. | |
| 122 | * | |
| 123 | * @see #locate(Function) | |
| 124 | */ | |
| 125 | public Optional<Path> locate() { | |
| 126 | return locate( System::getenv ); | |
| 144 | private Optional<Path> locate( | |
| 145 | final Function<String, String> map, final String msg ) { | |
| 146 | final var paths = paths( map ).split( quote( pathSeparator ) ); | |
| 147 | ||
| 148 | return locate( paths, msg ); | |
| 127 | 149 | } |
| 128 | 150 | |
| 129 | 151 | /** |
| 130 | * Provides {@code null}-safe machinery to get a file name. | |
| 152 | * Runs {@code where} or {@code which} to determine the fully qualified path | |
| 153 | * to an executable. | |
| 131 | 154 | * |
| 132 | * @param p The path to the file name to retrieve (may be {@code null}). | |
| 133 | * @return The file name or the empty string if the path is not found. | |
| 155 | * @return The path to the executable for this file, if found. | |
| 156 | * @throws IOException Could not determine the location of the command. | |
| 134 | 157 | */ |
| 135 | public static String getFileName( final Path p ) { | |
| 136 | return p == null ? "" : getPathFileName( p ); | |
| 137 | } | |
| 138 | ||
| 139 | private static String getPathFileName( final Path p ) { | |
| 140 | assert p != null; | |
| 141 | ||
| 142 | final var f = p.getFileName(); | |
| 158 | public Optional<Path> where() throws IOException { | |
| 159 | // The "where" command on Windows will automatically add the extension. | |
| 160 | final var args = new String[]{WHERE_COMMAND, getName()}; | |
| 161 | final var output = run( text -> true, args ); | |
| 162 | final var result = output.lines().findFirst(); | |
| 143 | 163 | |
| 144 | return f == null ? "" : f.toString(); | |
| 164 | return result.map( Path::of ); | |
| 145 | 165 | } |
| 146 | 166 | |
| ... | ||
| 153 | 173 | * @return The revised PATH variables as stored in the registry. |
| 154 | 174 | */ |
| 155 | private String paths( final Function<String, String> map ) { | |
| 175 | private static String paths( final Function<String, String> map ) { | |
| 156 | 176 | return IS_OS_WINDOWS ? pathsWindows( map ) : pathsSane(); |
| 157 | 177 | } |
| 158 | 178 | |
| 159 | private String pathsSane() { | |
| 160 | return getenv( "PATH" ); | |
| 161 | } | |
| 179 | /** | |
| 180 | * Answers whether this file's SHA-256 checksum equals the given | |
| 181 | * hexadecimal-encoded checksum string. | |
| 182 | * | |
| 183 | * @param hex The string to compare against the checksum for this file. | |
| 184 | * @return {@code true} if the checksums match; {@code false} on any | |
| 185 | * error or checksums don't match. | |
| 186 | */ | |
| 187 | public boolean isChecksum( final String hex ) { | |
| 188 | assert hex != null; | |
| 162 | 189 | |
| 163 | @SuppressWarnings( "SpellCheckingInspection" ) | |
| 164 | private String pathsWindows( final Function<String, String> map ) { | |
| 165 | 190 | try { |
| 166 | final var hklm = query( SYS_KEY ); | |
| 167 | final var hkcu = query( USR_KEY ); | |
| 191 | return checksum( "SHA-256" ).equalsIgnoreCase( hex ); | |
| 192 | } catch( final Exception ex ) { | |
| 193 | return false; | |
| 194 | } | |
| 195 | } | |
| 168 | 196 | |
| 169 | return expand( hklm, map ) + pathSeparator + expand( hkcu, map ); | |
| 170 | } catch( final IOException ex ) { | |
| 171 | // Return the PATH environment variable if the registry query fails. | |
| 172 | return pathsSane(); | |
| 197 | /** | |
| 198 | * Returns the hash code for this file. | |
| 199 | * | |
| 200 | * @return The hex-encoded hash code for the file contents. | |
| 201 | */ | |
| 202 | @SuppressWarnings( "SameParameterValue" ) | |
| 203 | private String checksum( final String algorithm ) | |
| 204 | throws NoSuchAlgorithmException, IOException { | |
| 205 | final var digest = MessageDigest.getInstance( algorithm ); | |
| 206 | ||
| 207 | try( final var in = new FileInputStream( this ) ) { | |
| 208 | final var bytes = new byte[ BUFFER_SIZE ]; | |
| 209 | int count; | |
| 210 | ||
| 211 | while( (count = in.read( bytes )) != -1 ) { | |
| 212 | digest.update( bytes, 0, count ); | |
| 213 | } | |
| 214 | ||
| 215 | return toHex( digest.digest() ); | |
| 173 | 216 | } |
| 174 | 217 | } |
| 175 | 218 | |
| 176 | 219 | /** |
| 177 | * Queries a registry key PATH value. | |
| 220 | * Runs a command and collects standard output into a buffer. | |
| 178 | 221 | * |
| 179 | * @param key The registry key name to look up. | |
| 180 | * @return The value for the registry key. | |
| 222 | * @param filter Provides an injected test to determine whether the line | |
| 223 | * read from the command's standard output is to be added to | |
| 224 | * the result buffer. | |
| 225 | * @param args The command and its arguments to run. | |
| 226 | * @return The standard output from the command, filtered. | |
| 227 | * @throws IOException Could not run the command. | |
| 181 | 228 | */ |
| 182 | private String query( final String key ) throws IOException { | |
| 183 | final var regVarName = "path"; | |
| 184 | final var args = new String[]{"reg", "query", key, "/v", regVarName}; | |
| 229 | @NotNull | |
| 230 | public static String run( final Predicate<String> filter, | |
| 231 | final String[] args ) throws IOException { | |
| 185 | 232 | final var process = Runtime.getRuntime().exec( args ); |
| 186 | 233 | final var stream = process.getInputStream(); |
| 187 | final var regValue = new StringBuffer( 1024 ); | |
| 234 | final var stdout = new StringBuffer( 2048 ); | |
| 188 | 235 | |
| 189 | 236 | StreamGobbler.gobble( stream, text -> { |
| 190 | if( text.contains( regVarName ) ) { | |
| 191 | regValue.append( parseRegEntry( text ) ); | |
| 237 | if( filter.test( text ) ) { | |
| 238 | stdout.append( WindowsRegistry.parseRegEntry( text ) ); | |
| 192 | 239 | } |
| 193 | 240 | } ); |
| ... | ||
| 200 | 247 | process.destroy(); |
| 201 | 248 | } |
| 202 | ||
| 203 | ||
| 204 | return regValue.toString(); | |
| 205 | } | |
| 206 | ||
| 207 | String parseRegEntry( final String text ) { | |
| 208 | assert text != null; | |
| 209 | 249 | |
| 210 | final var matcher = REG_PATTERN.matcher( text ); | |
| 211 | return matcher.find() ? matcher.group( 1 ) : text.trim(); | |
| 250 | return stdout.toString(); | |
| 212 | 251 | } |
| 213 | 252 | |
| 214 | 253 | /** |
| 215 | * PATH environment variables returned from the registry have unexpanded | |
| 216 | * variables of the form %VARIABLE%. This method will expand those values, | |
| 217 | * if possible, from the environment. This will only perform a single | |
| 218 | * expansion, which should be adequate for most needs. | |
| 254 | * Provides {@code null}-safe machinery to get a file name. | |
| 219 | 255 | * |
| 220 | * @param s The %VARIABLE%-encoded value to expand. | |
| 221 | * @return The given value with all encoded values expanded. | |
| 256 | * @param p The path to the file name to retrieve (may be {@code null}). | |
| 257 | * @return The file name or the empty string if the path is not found. | |
| 222 | 258 | */ |
| 223 | String expand( final String s, final Function<String, String> map ) { | |
| 224 | // Assigned to the unexpanded string, initially. | |
| 225 | String expanded = s; | |
| 226 | ||
| 227 | final var matcher = VAR_PATTERN.matcher( expanded ); | |
| 228 | ||
| 229 | while( matcher.find() ) { | |
| 230 | final var match = matcher.group( 0 ); | |
| 231 | String value = map.apply( match ); | |
| 232 | ||
| 233 | if( value == null ) { | |
| 234 | value = ""; | |
| 235 | } | |
| 236 | else { | |
| 237 | value = value.replace( "\\", "\\\\" ); | |
| 238 | } | |
| 239 | ||
| 240 | final var subexpression = compile( quote( match ) ); | |
| 241 | expanded = subexpression.matcher( expanded ).replaceAll( value ); | |
| 242 | } | |
| 243 | ||
| 244 | return expanded; | |
| 259 | public static String getFileName( final Path p ) { | |
| 260 | return p == null ? "" : getPathFileName( p ); | |
| 245 | 261 | } |
| 246 | 262 | |
| 247 | 263 | /** |
| 248 | * Answers whether this file's SHA-256 checksum equals the given | |
| 249 | * hexadecimal-encoded checksum string. | |
| 264 | * If the path doesn't exist right before typesetting, switch the path | |
| 265 | * to the user's home directory to increase the odds of the typesetter | |
| 266 | * succeeding. This could help, for example, if the images directory was | |
| 267 | * deleted or moved. | |
| 250 | 268 | * |
| 251 | * @param hex The string to compare against the checksum for this file. | |
| 252 | * @return {@code true} if the checksums match; {@code false} on any | |
| 253 | * error or checksums don't match. | |
| 269 | * @param path The path to verify existence. | |
| 270 | * @return The given path, if it exists, otherwise the user's home directory. | |
| 254 | 271 | */ |
| 255 | public boolean isChecksum( final String hex ) { | |
| 256 | assert hex != null; | |
| 272 | public static Path normalize( final Path path ) { | |
| 273 | assert path != null; | |
| 257 | 274 | |
| 258 | try { | |
| 259 | return checksum( "SHA-256" ).equalsIgnoreCase( hex ); | |
| 260 | } catch( final Exception ex ) { | |
| 261 | return false; | |
| 262 | } | |
| 275 | return path.toFile().exists() ? path : USER_DIRECTORY.toPath(); | |
| 263 | 276 | } |
| 264 | 277 | |
| 265 | /** | |
| 266 | * Returns the hash code for this file. | |
| 267 | * | |
| 268 | * @return The hex-encoded hash code for the file contents. | |
| 269 | */ | |
| 270 | @SuppressWarnings( "SameParameterValue" ) | |
| 271 | private String checksum( final String algorithm ) | |
| 272 | throws NoSuchAlgorithmException, IOException { | |
| 273 | final var digest = MessageDigest.getInstance( algorithm ); | |
| 278 | private static String pathsSane() { | |
| 279 | return getenv( "PATH" ); | |
| 280 | } | |
| 274 | 281 | |
| 275 | try( final var in = new FileInputStream( this ) ) { | |
| 276 | final var bytes = new byte[ BUFFER_SIZE ]; | |
| 277 | int count; | |
| 282 | private static String getPathFileName( final Path p ) { | |
| 283 | assert p != null; | |
| 278 | 284 | |
| 279 | while( (count = in.read( bytes )) != -1 ) { | |
| 280 | digest.update( bytes, 0, count ); | |
| 281 | } | |
| 285 | final var f = p.getFileName(); | |
| 282 | 286 | |
| 283 | return toHex( digest.digest() ); | |
| 284 | } | |
| 287 | return f == null ? "" : f.toString(); | |
| 285 | 288 | } |
| 286 | 289 | } |
| 1 | /* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.io; | |
| 3 | ||
| 4 | import java.io.IOException; | |
| 5 | import java.util.function.Function; | |
| 6 | import java.util.regex.Pattern; | |
| 7 | ||
| 8 | import static java.io.File.pathSeparator; | |
| 9 | import static java.lang.System.getenv; | |
| 10 | import static java.util.regex.Pattern.compile; | |
| 11 | import static java.util.regex.Pattern.quote; | |
| 12 | ||
| 13 | /** | |
| 14 | * Responsible for obtaining Windows registry key values. | |
| 15 | */ | |
| 16 | public class WindowsRegistry { | |
| 17 | //@formatter:off | |
| 18 | private static final String SYS_KEY = | |
| 19 | "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"; | |
| 20 | private static final String USR_KEY = | |
| 21 | "HKEY_CURRENT_USER\\Environment"; | |
| 22 | //@formatter:on | |
| 23 | ||
| 24 | /** | |
| 25 | * Regular expression pattern for matching %VARIABLE% names. | |
| 26 | */ | |
| 27 | private static final String VAR_REGEX = "%.*?%"; | |
| 28 | private static final Pattern VAR_PATTERN = compile( VAR_REGEX ); | |
| 29 | ||
| 30 | private static final String REG_REGEX = "\\s*path\\s+REG_EXPAND_SZ\\s+(.*)"; | |
| 31 | private static final Pattern REG_PATTERN = compile( REG_REGEX ); | |
| 32 | ||
| 33 | /** | |
| 34 | * Returns the value of the Windows PATH registry key. | |
| 35 | * | |
| 36 | * @return The PATH environment variable if the registry query fails. | |
| 37 | */ | |
| 38 | @SuppressWarnings( "SpellCheckingInspection" ) | |
| 39 | public static String pathsWindows( final Function<String, String> map ) { | |
| 40 | try { | |
| 41 | final var hklm = query( SYS_KEY ); | |
| 42 | final var hkcu = query( USR_KEY ); | |
| 43 | ||
| 44 | return expand( hklm, map ) + pathSeparator + expand( hkcu, map ); | |
| 45 | } catch( final IOException ex ) { | |
| 46 | return getenv( "PATH" ); | |
| 47 | } | |
| 48 | } | |
| 49 | ||
| 50 | /** | |
| 51 | * Queries a registry key PATH value. | |
| 52 | * | |
| 53 | * @param key The registry key name to look up. | |
| 54 | * @return The value for the registry key. | |
| 55 | */ | |
| 56 | private static String query( final String key ) throws IOException { | |
| 57 | final var registryVarName = "path"; | |
| 58 | final var args = new String[]{"reg", "query", key, "/v", registryVarName}; | |
| 59 | ||
| 60 | return SysFile.run( text -> text.contains( registryVarName ), args ); | |
| 61 | } | |
| 62 | ||
| 63 | static String parseRegEntry( final String text ) { | |
| 64 | assert text != null; | |
| 65 | ||
| 66 | final var matcher = REG_PATTERN.matcher( text ); | |
| 67 | return matcher.find() ? matcher.group( 1 ) : text.trim(); | |
| 68 | } | |
| 69 | ||
| 70 | /** | |
| 71 | * PATH environment variables returned from the registry have unexpanded | |
| 72 | * variables of the form %VARIABLE%. This method will expand those values, | |
| 73 | * if possible, from the environment. This will only perform a single | |
| 74 | * expansion, which should be adequate for most needs. | |
| 75 | * | |
| 76 | * @param s The %VARIABLE%-encoded value to expand. | |
| 77 | * @return The given value with all encoded values expanded. | |
| 78 | */ | |
| 79 | static String expand( final String s, final Function<String, String> map ) { | |
| 80 | // Assigned to the unexpanded string, initially. | |
| 81 | String expanded = s; | |
| 82 | ||
| 83 | final var matcher = VAR_PATTERN.matcher( expanded ); | |
| 84 | ||
| 85 | while( matcher.find() ) { | |
| 86 | final var match = matcher.group( 0 ); | |
| 87 | String value = map.apply( match ); | |
| 88 | ||
| 89 | if( value == null ) { | |
| 90 | value = ""; | |
| 91 | } | |
| 92 | else { | |
| 93 | value = value.replace( "\\", "\\\\" ); | |
| 94 | } | |
| 95 | ||
| 96 | final var subexpression = compile( quote( match ) ); | |
| 97 | expanded = subexpression.matcher( expanded ).replaceAll( value ); | |
| 98 | } | |
| 99 | ||
| 100 | return expanded; | |
| 101 | } | |
| 102 | } | |
| 1 | 103 |
| 7 | 7 | import static com.keenwrite.events.StatusEvent.clue; |
| 8 | 8 | import static com.keenwrite.io.MediaType.TEXT_XML; |
| 9 | import static com.keenwrite.io.SysFile.normalize; | |
| 9 | 10 | import static com.keenwrite.typesetting.Typesetter.Mutator; |
| 10 | 11 | import static java.nio.file.Files.deleteIfExists; |
| ... | ||
| 39 | 40 | clue( "Main.status.typeset.setting", "target", targetPath ); |
| 40 | 41 | |
| 41 | final var parent = targetPath.toAbsolutePath().getParent(); | |
| 42 | final var parent = normalize( targetPath.toAbsolutePath().getParent() ); | |
| 42 | 43 | |
| 43 | 44 | final var document = TEXT_XML.createTempFile( APP_TITLE_ABBR, parent ); |
| 44 | 45 | final var sourcePath = writeString( document, xhtml ); |
| 45 | 46 | clue( "Main.status.typeset.setting", "source", sourcePath ); |
| 46 | 47 | |
| 47 | final var themeDir = context.getThemeDir(); | |
| 48 | final var themeDir = normalize( context.getThemeDir() ); | |
| 48 | 49 | clue( "Main.status.typeset.setting", "themes", themeDir ); |
| 49 | 50 | |
| 50 | final var imageDir = context.getImageDir(); | |
| 51 | final var imageDir = normalize( context.getImageDir() ); | |
| 51 | 52 | clue( "Main.status.typeset.setting", "images", imageDir ); |
| 52 | 53 | |
| 53 | final var cacheDir = context.getCacheDir(); | |
| 54 | final var imageOrder = context.getImageOrder(); | |
| 55 | clue( "Main.status.typeset.setting", "order", imageOrder ); | |
| 56 | ||
| 57 | final var cacheDir = normalize( context.getCacheDir() ); | |
| 54 | 58 | clue( "Main.status.typeset.setting", "caches", cacheDir ); |
| 55 | 59 | |
| 56 | final var fontDir = context.getFontDir(); | |
| 60 | final var fontDir = normalize( context.getFontDir() ); | |
| 57 | 61 | clue( "Main.status.typeset.setting", "fonts", fontDir ); |
| 58 | ||
| 59 | final var autoRemove = context.getAutoRemove(); | |
| 60 | clue( "Main.status.typeset.setting", "purge", autoRemove ); | |
| 61 | 62 | |
| 62 | final var rWorkDir = context.getRWorkingDir(); | |
| 63 | final var rWorkDir = normalize( context.getRWorkingDir() ); | |
| 63 | 64 | clue( "Main.status.typeset.setting", "r-work", rWorkDir ); |
| 64 | 65 | |
| 65 | final var imageOrder = context.getImageOrder(); | |
| 66 | clue( "Main.status.typeset.setting", "order", imageOrder ); | |
| 66 | final var autoRemove = context.getAutoRemove(); | |
| 67 | clue( "Main.status.typeset.setting", "purge", autoRemove ); | |
| 67 | 68 | |
| 68 | 69 | final var typesetter = Typesetter |
| 69 | 70 | .builder() |
| 70 | .with( Mutator::setSourcePath, sourcePath ) | |
| 71 | 71 | .with( Mutator::setTargetPath, targetPath ) |
| 72 | .with( Mutator::setSourcePath, sourcePath ) | |
| 72 | 73 | .with( Mutator::setThemeDir, themeDir ) |
| 73 | 74 | .with( Mutator::setImageDir, imageDir ) |
| 12 | 12 | import java.util.concurrent.Callable; |
| 13 | 13 | |
| 14 | import static com.keenwrite.constants.Constants.USER_DIRECTORY; | |
| 14 | import static com.keenwrite.events.StatusEvent.clue; | |
| 15 | 15 | import static com.keenwrite.io.StreamGobbler.gobble; |
| 16 | import static com.keenwrite.typesetting.containerization.Podman.MANAGER; | |
| 16 | import static com.keenwrite.io.SysFile.normalize; | |
| 17 | 17 | import static java.lang.String.format; |
| 18 | 18 | |
| ... | ||
| 81 | 81 | |
| 82 | 82 | return true; |
| 83 | } | |
| 84 | ||
| 85 | /** | |
| 86 | * If the path doesn't exist right before typesetting, switch the path | |
| 87 | * to the user's home directory to increase the odds of the typesetter | |
| 88 | * succeeding. This could help, for example, if the images directory was | |
| 89 | * deleted or moved. | |
| 90 | * | |
| 91 | * @param path The path to verify existence. | |
| 92 | * @return The given path, if it exists, otherwise the user's home directory. | |
| 93 | */ | |
| 94 | private static Path normalize( final Path path ) { | |
| 95 | assert path != null; | |
| 96 | ||
| 97 | return path.toFile().exists() | |
| 98 | ? path | |
| 99 | : USER_DIRECTORY.toPath(); | |
| 100 | 83 | } |
| 101 | 84 | |
| ... | ||
| 109 | 92 | */ |
| 110 | 93 | static boolean isReady() { |
| 111 | if( MANAGER.canRun() ) { | |
| 94 | if( Podman.canRun() ) { | |
| 112 | 95 | final var exitCode = new StringBuilder(); |
| 113 | 96 | final var manager = new Podman(); |
| ... | ||
| 122 | 105 | // If the typesetter ran with an exit code of 0, it is available. |
| 123 | 106 | return exitCode.indexOf( "0" ) == 0; |
| 124 | } catch( final CommandNotFoundException ignored ) { } | |
| 107 | } catch( final CommandNotFoundException ex ) { | |
| 108 | clue( ex ); | |
| 109 | } | |
| 125 | 110 | } |
| 126 | 111 | |
| 6 | 6 | |
| 7 | 7 | import java.io.File; |
| 8 | import java.io.IOException; | |
| 8 | import java.nio.file.Files; | |
| 9 | 9 | import java.nio.file.Path; |
| 10 | 10 | import java.util.LinkedList; |
| 11 | 11 | import java.util.List; |
| 12 | import java.util.NoSuchElementException; | |
| 13 | 12 | |
| 14 | 13 | import static com.keenwrite.Bootstrap.CONTAINER_VERSION; |
| 14 | import static com.keenwrite.events.StatusEvent.clue; | |
| 15 | 15 | import static java.lang.String.format; |
| 16 | import static java.lang.String.join; | |
| 16 | 17 | import static java.lang.System.arraycopy; |
| 17 | 18 | import static java.util.Arrays.copyOf; |
| 19 | import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS; | |
| 18 | 20 | |
| 19 | 21 | /** |
| 20 | 22 | * Provides facilities for interacting with a container environment. |
| 21 | 23 | */ |
| 22 | 24 | public final class Podman implements ContainerManager { |
| 23 | public static final SysFile MANAGER = new SysFile( "podman" ); | |
| 25 | private static final String BINARY = "podman"; | |
| 26 | private static final Path BINARY_PATH = | |
| 27 | Path.of( | |
| 28 | format( IS_OS_WINDOWS | |
| 29 | ? "C:\\Program Files\\RedHat\\Podman\\%s.exe" | |
| 30 | : "/usr/bin/%s", | |
| 31 | BINARY | |
| 32 | ) | |
| 33 | ); | |
| 34 | private static final SysFile MANAGER = new SysFile( BINARY ); | |
| 35 | ||
| 24 | 36 | public static final String CONTAINER_SHORTNAME = "typesetter"; |
| 25 | 37 | public static final String CONTAINER_NAME = |
| 26 | 38 | format( "%s:%s", CONTAINER_SHORTNAME, CONTAINER_VERSION ); |
| 27 | 39 | |
| 28 | 40 | private final List<String> mMountPoints = new LinkedList<>(); |
| 29 | 41 | |
| 30 | 42 | public Podman() { } |
| 43 | ||
| 44 | /** | |
| 45 | * Answers whether the container is installed and runnable on the host. | |
| 46 | * | |
| 47 | * @return {@code true} if the container is available. | |
| 48 | */ | |
| 49 | public static boolean canRun() { | |
| 50 | try { | |
| 51 | return getExecutable().toFile().isFile(); | |
| 52 | } catch( final Exception ex ) { | |
| 53 | clue( "Wizard.container.executable.run.error", ex ); | |
| 54 | ||
| 55 | // If the binary couldn't be found, then indicate that it cannot run. | |
| 56 | return false; | |
| 57 | } | |
| 58 | } | |
| 59 | ||
| 60 | private static Path getExecutable() { | |
| 61 | final var executable = Files.isExecutable( BINARY_PATH ); | |
| 62 | ||
| 63 | clue( "Wizard.container.executable.run.scan", BINARY_PATH, executable ); | |
| 64 | ||
| 65 | return executable | |
| 66 | ? BINARY_PATH | |
| 67 | : MANAGER.locate().orElseThrow(); | |
| 68 | } | |
| 31 | 69 | |
| 32 | 70 | @Override |
| 33 | 71 | public int install( final File exe ) { |
| 34 | 72 | // This monstrosity runs the installer in the background without displaying |
| 35 | 73 | // a secondary command window, while blocking until the installer completes |
| 36 | 74 | // and an exit code can be determined. I hate Windows. |
| 37 | final var builder = processBuilder( | |
| 38 | "cmd", "/c", | |
| 39 | format( | |
| 40 | "start /b /high /wait cmd /c %s /quiet /install & exit ^!errorlevel^!", | |
| 41 | exe.getAbsolutePath() | |
| 42 | ) | |
| 75 | final var cmd = format( | |
| 76 | "start /b /high /wait cmd /c %s /quiet /install & exit ^!errorlevel^!", | |
| 77 | exe.getAbsolutePath() | |
| 43 | 78 | ); |
| 79 | ||
| 80 | clue( "Wizard.container.install.command", cmd ); | |
| 81 | ||
| 82 | final var builder = processBuilder( "cmd", "/c", cmd ); | |
| 44 | 83 | |
| 45 | 84 | try { |
| 85 | clue( "Wizard.container.install.await", cmd ); | |
| 86 | ||
| 46 | 87 | // Wait for installation to finish (successfully or not). |
| 47 | 88 | return wait( builder.start() ); |
| ... | ||
| 128 | 169 | throws CommandNotFoundException { |
| 129 | 170 | try { |
| 130 | final var exe = MANAGER.locate(); | |
| 131 | final var path = exe.orElseThrow(); | |
| 171 | final var path = getExecutable(); | |
| 172 | final var joined = join( ",", args ); | |
| 173 | ||
| 174 | clue( "Wizard.container.process.enter", path, joined ); | |
| 175 | ||
| 132 | 176 | final var builder = processBuilder( path, args ); |
| 133 | 177 | final var process = builder.start(); |
| 134 | 178 | |
| 135 | 179 | processor.start( process.getInputStream() ); |
| 136 | 180 | |
| 137 | 181 | return wait( process ); |
| 138 | } catch( final NoSuchElementException | | |
| 139 | IOException | | |
| 140 | InterruptedException ex ) { | |
| 182 | } catch( final Exception ex ) { | |
| 183 | clue( ex ); | |
| 141 | 184 | throw new CommandNotFoundException( MANAGER.toString() ); |
| 142 | 185 | } |
| ... | ||
| 152 | 195 | private static int wait( final Process process ) throws InterruptedException { |
| 153 | 196 | final var exitCode = process.waitFor(); |
| 197 | ||
| 198 | clue( "Wizard.container.process.exit", exitCode ); | |
| 199 | ||
| 154 | 200 | process.destroy(); |
| 155 | 201 | |
| 1 | /* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.typesetting.installer; | |
| 3 | ||
| 4 | /** | |
| 5 | * Provides common constants across all panes. | |
| 6 | */ | |
| 7 | public class WizardConstants { | |
| 8 | ||
| 9 | ||
| 10 | private WizardConstants() { } | |
| 11 | } | |
| 12 | 1 |
| 53 | 53 | |
| 54 | 54 | if( thread instanceof Task<?> downloader && downloader.isRunning() ) { |
| 55 | clue( "Wizard.container.install.download.running" ); | |
| 55 | 56 | return; |
| 56 | 57 | } |
| ... | ||
| 70 | 71 | } |
| 71 | 72 | else { |
| 73 | clue( "Wizard.container.install.download.started", mUri ); | |
| 74 | ||
| 72 | 75 | final var task = downloadAsync( mUri, target, ( progress, bytes ) -> { |
| 73 | 76 | final var suffix = progress < 0 ? ".bytes" : ".progress"; |
| 271 | 271 | */ |
| 272 | 272 | private void file_export_pdf( final boolean dir ) { |
| 273 | final var workspace = getWorkspace(); | |
| 274 | final var themes = workspace.getFile( | |
| 275 | KEY_TYPESET_CONTEXT_THEMES_PATH | |
| 276 | ); | |
| 277 | final var theme = workspace.stringProperty( | |
| 278 | KEY_TYPESET_CONTEXT_THEME_SELECTION | |
| 279 | ); | |
| 280 | final var chapters = workspace.stringProperty( | |
| 281 | KEY_TYPESET_CONTEXT_CHAPTERS | |
| 282 | ); | |
| 283 | final var settings = ExportSettings | |
| 284 | .builder() | |
| 285 | .with( ExportSettings.Mutator::setTheme, theme ) | |
| 286 | .with( ExportSettings.Mutator::setChapters, chapters ) | |
| 287 | .build(); | |
| 288 | ||
| 289 | 273 | // Don't re-validate the typesetter installation each time. If the |
| 290 | 274 | // user mucks up the typesetter installation, it'll get caught the |
| 291 | 275 | // next time the application is started. Don't use |= because it |
| 292 | 276 | // won't short-circuit. |
| 293 | 277 | mCanTypeset = mCanTypeset || Typesetter.canRun(); |
| 294 | 278 | |
| 295 | 279 | if( mCanTypeset ) { |
| 280 | final var workspace = getWorkspace(); | |
| 281 | final var theme = workspace.stringProperty( | |
| 282 | KEY_TYPESET_CONTEXT_THEME_SELECTION | |
| 283 | ); | |
| 284 | final var chapters = workspace.stringProperty( | |
| 285 | KEY_TYPESET_CONTEXT_CHAPTERS | |
| 286 | ); | |
| 287 | ||
| 288 | final var settings = ExportSettings | |
| 289 | .builder() | |
| 290 | .with( ExportSettings.Mutator::setTheme, theme ) | |
| 291 | .with( ExportSettings.Mutator::setChapters, chapters ) | |
| 292 | .build(); | |
| 293 | ||
| 294 | final var themes = workspace.getFile( | |
| 295 | KEY_TYPESET_CONTEXT_THEMES_PATH | |
| 296 | ); | |
| 297 | ||
| 296 | 298 | // If the typesetter is installed, allow the user to select a theme. If |
| 297 | 299 | // the themes aren't installed, a status message will appear. |
| 1 | 1 | application.title=KeenWrite |
| 2 | container.version=2.11.5 | |
| 2 | container.version=3.0.0 | |
| 3 | 3 |
| 319 | 319 | Wizard.typesetter.name=ConTeXt |
| 320 | 320 | Wizard.typesetter.container.name=Podman |
| 321 | Wizard.typesetter.container.version=4.3.1 | |
| 322 | Wizard.typesetter.container.checksum=b741702663234ca36e1555149721580dc31ae76985d50c022a8641c6db2f5b93 | |
| 323 | Wizard.typesetter.themes.version=1.8.2 | |
| 324 | Wizard.typesetter.themes.checksum=00e0f46ea2cb4a812a4780b61a838ea94d78167c1abc9e16767401914a5e989d | |
| 321 | Wizard.typesetter.container.version=4.5.1 | |
| 322 | Wizard.typesetter.container.checksum=883c9901d1eedb3a130256574afa270656e4d322dce0bb880808d9d7776eff8a | |
| 323 | Wizard.typesetter.themes.version=1.8.3 | |
| 324 | Wizard.typesetter.themes.checksum=983b8d3c4051c6c002b8111ea786ebc83d5496ab5f8d734e96413c59304b2a75 | |
| 325 | ||
| 326 | Wizard.container.install.command=Installing container using: ''{0}'' | |
| 327 | Wizard.container.install.await=Waiting for installer to finish | |
| 328 | Wizard.container.install.download.started=Download ''{0}'' started | |
| 329 | Wizard.container.install.download.running=Download in progress, please wait | |
| 330 | Wizard.container.process.enter=Running ''{0}'' ''{1}'' | |
| 331 | Wizard.container.process.exit=Process exit code (zero means success): {0} | |
| 332 | Wizard.container.executable.run.scan=''{0}'' is executable: {1} | |
| 333 | Wizard.container.executable.run.error=Cannot run container | |
| 334 | Wizard.container.executable.which=Cannot find container using search command | |
| 335 | Wizard.container.executable.path=Cannot find container using PATH variable | |
| 336 | Wizard.container.executable.registry=Cannot find container using registry | |
| 325 | 337 | |
| 326 | 338 | # STEP 1: Introduction panel (all) |
| 4 | 4 | import org.junit.jupiter.api.Test; |
| 5 | 5 | |
| 6 | import static org.junit.jupiter.api.Assertions.assertEquals; | |
| 7 | import static org.junit.jupiter.api.Assertions.assertTrue; | |
| 6 | import java.io.IOException; | |
| 7 | import java.nio.file.Path; | |
| 8 | import java.util.Optional; | |
| 9 | import java.util.concurrent.atomic.AtomicBoolean; | |
| 10 | import java.util.function.Function; | |
| 11 | ||
| 12 | import static org.junit.jupiter.api.Assertions.*; | |
| 8 | 13 | |
| 9 | 14 | class SysFileTest { |
| 10 | private static final String REG_PATH_PREFIX = | |
| 11 | "%USERPROFILE%"; | |
| 12 | private static final String REG_PATH_SUFFIX = | |
| 13 | "\\AppData\\Local\\Microsoft\\WindowsApps;"; | |
| 14 | private static final String REG_PATH = REG_PATH_PREFIX + REG_PATH_SUFFIX; | |
| 15 | 15 | |
| 16 | 16 | @Test |
| 17 | 17 | void test_Locate_ExistingExecutable_PathFound() { |
| 18 | final var command = "ls"; | |
| 19 | final var file = new SysFile( command ); | |
| 20 | assertTrue( file.canRun() ); | |
| 21 | ||
| 22 | final var located = file.locate(); | |
| 23 | assertTrue( located.isPresent() ); | |
| 24 | ||
| 25 | final var path = located.get(); | |
| 26 | final var actual = path.toAbsolutePath().toString(); | |
| 27 | final var expected = "/usr/bin/" + command; | |
| 28 | ||
| 29 | assertEquals( expected, actual ); | |
| 18 | testFunction( SysFile::locate, "ls", "/usr/bin/ls" ); | |
| 30 | 19 | } |
| 31 | 20 | |
| 32 | 21 | @Test |
| 33 | void test_Parse_RegistryEntry_ValueObtained() { | |
| 34 | final var file = new SysFile( "unused" ); | |
| 35 | final var expected = REG_PATH; | |
| 36 | final var actual = | |
| 37 | file.parseRegEntry( " path REG_EXPAND_SZ " + expected ); | |
| 38 | ||
| 39 | assertEquals( expected, actual ); | |
| 22 | void test_Where_ExistingExecutable_PathFound() { | |
| 23 | testFunction( sysFile -> { | |
| 24 | try { | |
| 25 | return sysFile.where(); | |
| 26 | } catch( final IOException e ) { | |
| 27 | throw new RuntimeException( e ); | |
| 28 | } | |
| 29 | }, "which", "/usr/bin/which" ); | |
| 40 | 30 | } |
| 41 | 31 | |
| 42 | @Test | |
| 43 | void test_Expand_RegistryEntry_VariablesExpanded() { | |
| 44 | final var value = "UserProfile"; | |
| 45 | final var file = new SysFile( "unused" ); | |
| 46 | final var expected = value + REG_PATH_SUFFIX; | |
| 47 | final var actual = file.expand( REG_PATH, s -> value ); | |
| 32 | void testFunction( final Function<SysFile, Optional<Path>> consumer, | |
| 33 | final String command, | |
| 34 | final String expected ) { | |
| 35 | final var file = new SysFile( command ); | |
| 36 | final var path = consumer.apply( file ); | |
| 37 | final var failed = new AtomicBoolean( false ); | |
| 48 | 38 | |
| 49 | assertEquals( expected, actual ); | |
| 39 | assertTrue( file.canRun() ); | |
| 40 | ||
| 41 | path.ifPresentOrElse( | |
| 42 | location -> { | |
| 43 | final var actual = location.toAbsolutePath().toString(); | |
| 44 | ||
| 45 | assertEquals( expected, actual ); | |
| 46 | }, | |
| 47 | () -> failed.set( true ) | |
| 48 | ); | |
| 49 | ||
| 50 | assertFalse( failed.get() ); | |
| 50 | 51 | } |
| 51 | 52 | } |
| 1 | package com.keenwrite.io; | |
| 2 | ||
| 3 | import org.junit.jupiter.api.Test; | |
| 4 | ||
| 5 | import static com.keenwrite.io.WindowsRegistry.*; | |
| 6 | import static org.junit.jupiter.api.Assertions.*; | |
| 7 | ||
| 8 | class WindowsRegistryTest { | |
| 9 | private static final String REG_PATH_PREFIX = | |
| 10 | "%USERPROFILE%"; | |
| 11 | private static final String REG_PATH_SUFFIX = | |
| 12 | "\\AppData\\Local\\Microsoft\\WindowsApps;"; | |
| 13 | private static final String REG_PATH = REG_PATH_PREFIX + REG_PATH_SUFFIX; | |
| 14 | ||
| 15 | @Test | |
| 16 | void test_Parse_RegistryEntry_ValueObtained() { | |
| 17 | final var expected = REG_PATH; | |
| 18 | final var actual = parseRegEntry( | |
| 19 | " path REG_EXPAND_SZ " + expected | |
| 20 | ); | |
| 21 | ||
| 22 | assertEquals( expected, actual ); | |
| 23 | } | |
| 24 | ||
| 25 | @Test | |
| 26 | void test_Expand_RegistryEntry_VariablesExpanded() { | |
| 27 | final var value = "UserProfile"; | |
| 28 | final var expected = value + REG_PATH_SUFFIX; | |
| 29 | final var actual = expand( REG_PATH, s -> value ); | |
| 30 | ||
| 31 | assertEquals( expected, actual ); | |
| 32 | } | |
| 33 | } | |
| 1 | 34 |