View Javadoc

1   package com.tapina.robe.swi;
2   
3   import com.tapina.robe.module.Module;
4   import com.tapina.robe.module.ModuleUtils;
5   import com.tapina.robe.runtime.*;
6   import com.tapina.robe.runtime.instruction.SWI;
7   import com.tapina.robe.swi.os.*;
8   
9   import java.awt.*;
10  import java.awt.image.BufferedImage;
11  import java.io.*;
12  import java.lang.reflect.Method;
13  import java.text.CharacterIterator;
14  import java.util.*;
15  import java.util.logging.Logger;
16  
17  /***
18   * Created by IntelliJ IDEA.
19   * User: gareth
20   * Date: Aug 21, 2003
21   * Time: 6:12:53 PM
22   */
23  public final class OS extends SWIHandler {
24      private static OS instance = null;
25  
26      private OS() {
27      }
28  
29      public synchronized static OS getInstance() {
30          if (instance == null) {
31              instance = new OS();
32          }
33          return instance;
34      }
35  
36      public static int getBase() {
37          return 0;
38      }
39  
40      public String getName() {
41          return "UtilityModule";
42      }
43  
44      private static final String[] methodNames = new String[]{
45          "WriteC", "", "Write0", "NewLine", "ReadC", "CLI", "Byte", "", // 0x00
46          "File", "Args", "", "", "GBPB", "Find", "", "", // 0x08
47          "GetEnv", "Exit", "", "IntOn", "IntOff", "", "EnterOS", "", // 0x10
48          "", "", "", "", "", "", "Module", "", // 0x18
49          "", "ReadUnsigned", "", "ReadVarVal", "SetVarVal", "", "", "GSTrans", // 0x20
50          "", "FSControl", "ChangeDynamicArea", "GenerateError", "", "", "SpriteOp", "", // 0x28
51          "", "ReadVduVariables", "", "", "", "ReadModeVariable", "", "", // 0x30
52          "", "", "", "", "", "", "", "" // 0x38
53      };
54  
55      public static Method getMethod(Integer offset) throws NoSuchMethodException {
56          return getMethod(methodNames[offset.intValue()]);
57      }
58  
59      private static final Method getMethod(String name) throws NoSuchMethodException {
60          return OS.class.getMethod(name, METHOD_PARAMETERS);
61      }
62  
63      public final void WriteC(Environment env) {
64          char charIso88591 = (char) (env.getCpu().R[0] & 0xff);
65          writeC(charIso88591);
66      }
67  
68      public final void writeC(char c) {
69          System.out.print(c);
70      }
71  
72      public final void NewLine(Environment env) {
73          newLine();
74      }
75  
76      public final void newLine() {
77          System.out.print("\r\n");
78      }
79  
80      public final void ReadC(Environment env) {
81          char charIso88591 = readC();
82          env.getCpu().clearC();
83          env.getCpu().R[0] = charIso88591 & 0xff;
84      }
85  
86      public final char readC() {
87          try {
88              return (char) System.in.read();
89          } catch (IOException e) {
90              throw new SWIError(0, e);
91          }
92      }
93  
94      public final void Write0(Environment env) {
95          final int[] R = env.getCpu().R;
96          final String s = env.getMemoryMap().getString0(R[0]);
97          write0(s);
98          R[0] += s.length() + 1;
99      }
100 
101     public final void write0(String s) {
102         System.out.print(s);
103     }
104 
105     public final void CLI(Environment env) {
106         String command = env.getMemoryMap().getString0_10_13(env.getCpu().R[0]);
107         cli(command);
108     }
109 
110     public final void cli(String command) {
111         StringTokenizer tokenizer = new StringTokenizer(command);
112         String id = tokenizer.nextToken();
113         if (id.equalsIgnoreCase("copy")) {
114             final String fromFile = tokenizer.nextToken();
115             final String toFile = tokenizer.nextToken();
116             boolean preserveAccess = true, confirmCopy = true, deleteSource = false, forceOverwrite = false,
117                     lookFirst = false, newerOnly = false, promptDiscChange = false, quick = false, recurse = false,
118                     restamp = false, structureOnly = false, verbose = true;
119             if (tokenizer.hasMoreTokens()) {
120                 final String options = tokenizer.nextToken().toUpperCase();
121                 if (options.indexOf("~A") != -1) preserveAccess = false;
122                 if (options.indexOf("~C") != -1) confirmCopy = false;
123                 if (options.indexOf("D") != -1) deleteSource = true;
124                 if (options.indexOf("F") != -1) forceOverwrite = true;
125                 if (options.indexOf("L") != -1) lookFirst = true;
126                 if (options.indexOf("N") != -1) newerOnly = true;
127                 if (options.indexOf("P") != -1) promptDiscChange = true;
128                 if (options.indexOf("Q") != -1) quick = true;
129                 if (options.indexOf("R") != -1) recurse = true;
130                 if (options.indexOf("S") != -1) restamp = true;
131                 if (options.indexOf("T") != -1) structureOnly = true;
132                 if (options.indexOf("~V") != -1) verbose = false;
133                 File in = new File(FilenameUtils.acornToNative(fromFile));
134                 File out = new File(FilenameUtils.acornToNative(toFile));
135                 BufferedInputStream inputStream = null;
136                 BufferedOutputStream outputStream = null;
137                 try {
138                     inputStream = new BufferedInputStream(new FileInputStream(in));
139                     outputStream = new BufferedOutputStream(new FileOutputStream(out));
140                     int c;
141                     while ((c = inputStream.read()) != -1) {
142                         outputStream.write(c);
143                     }
144                 } catch (IOException e) {
145                     throw new SWIError(0, e);
146                 } finally {
147                     try {
148                         if (inputStream != null) {
149                             inputStream.close();
150                         }
151                         if (outputStream != null) {
152                             outputStream.close();
153                         }
154                     } catch (IOException e) {
155                         // Ignore
156                     }
157                 }
158                 if (deleteSource) {
159                     in.delete();
160                 }
161             }
162         } else {
163             log.warning("We do not handle * command: *" + command);
164         }
165     }
166 
167     public final void Byte(Environment env) {
168         final int[] R = env.getCpu().R;
169         int[] result = osByte(R[0], R[1], R[2]);
170         System.arraycopy(result, 0, R, 1, result.length);
171     }
172 
173     private final static byte[] CMOS = new byte[240];
174 
175     static {
176         CMOS[140] = 0; // NewLook flags
177     }
178 
179     public final int[] osByte(int reasonCode, int r1, int r2) {
180         switch (reasonCode) {
181             case 0: // Get OS version
182                 switch (r1) {
183                     case 0: // Throw version as error
184                         throw new SWIError(0, "ROBE 0.10");
185                     case 1: // Return version code
186                         return new int[]{6};
187                     default:
188                         throw new SWIError(0, "Unknown OS.Byte 0," + r1);
189                 }
190             case 161: // Read CMOS value
191                 return new int[]{r1, CMOS[r1]};
192             case 240: // Read country number
193                 if (r1 != 0 || r2 != 255) {
194                     log.warning("We do not support writing country number with OSByte 240");
195                 }
196                 return new int[]{Territory.getInstance().getDefaultTerritoryNumber(), 0};
197             default:
198                 throw new SWIError(0, "Unknown OS.Byte " + reasonCode);
199         }
200     }
201 
202     public final void File(Environment env) {
203         final int[] R = env.getCpu().R;
204         final String filename = env.getMemoryMap().getString0(R[1]);
205         switch (R[0]) {
206             case 1:
207                 file1(filename, R[2], R[3], R[5]);
208                 break;
209             case 8:
210                 file8(filename);
211                 break;
212             case 10:
213                 ByteArray data = env.getMemoryMap().getByteArray(R[4], R[5] - R[4] + 1, false);
214                 file10(filename, R[2], data);
215                 break;
216             case 16:
217                 if ((R[3] & 0xff) != 0) {
218                     throw new SWIError(0, "Cannot load files at their own load address");
219                 }
220                 if ((R[3] & 1<<31) != 0) {
221                     log.warning("We do not correctly synchronise on code load");
222                 }
223                 final DirectoryEntry entry16 = file5(filename, null);
224                 ByteArray buffer = env.getMemoryMap().getByteArray(R[2], entry16.getLength(), true);
225                 file16(filename, buffer);
226                 break;
227             case 17:
228                 DirectoryEntry entry = file5(filename, null);
229                 if (entry != null) {
230                     R[0] = entry.getObjectType();
231                     R[2] = entry.getLoadAddress();
232                     R[3] = entry.getExecAddress();
233                     R[4] = entry.getLength();
234                     R[5] = entry.getAttributes();
235                 } else {
236                     R[0] = 0; // Not found.
237                 }
238                 break;
239             default:
240                 throw new SWIError(0, "Unknown OS.File " + R[0]);
241         }
242     }
243 
244     /***
245      * This SWI is used to load a named file. An error will be generated if not found, is directory, no read access,
246      * or a bad load address.
247      * @param filename
248      * @param buffer
249      */
250     private void file16(String filename, ByteArray buffer) {
251         final File file = new File(FilenameUtils.acornToNative(filename));
252         if (file.isDirectory()) {
253             throw new SWIError(0, "Filename specified is a directory");
254         }
255         FileInputStream inputStream = null;
256         try {
257             inputStream = new FileInputStream(file);
258             inputStream.read(buffer.getArray(), buffer.getOffset(), buffer.getLength());
259             inputStream.close();
260         } catch (IOException e) {
261             throw new SWIError(0, e);
262         } finally {
263             if (inputStream != null) {
264                 try {
265                     inputStream.close();
266                 } catch (IOException e) { // Ignore
267                 }
268             }
269         }
270     }
271 
272     /***
273      * This SWI is used to read catalogue information for a named object.
274      *
275      * @param filename
276      * @param path
277      */
278     public final DirectoryEntry file5(String filename, String path) {
279         if (path != null) {
280             log.warning("OS_File path ignored when reading directory entry");
281         }
282         File file = new File(FilenameUtils.acornToNative(filename));
283         return fileDirectoryEntry(file);
284     }
285 
286     public void file1(String filename, int loadAddress, int execAddress, int attributes) {
287         DirectoryEntry directoryEntry = new DirectoryEntry(filename, loadAddress, execAddress, attributes);
288         File file = new File(filename);
289         file.setLastModified(directoryEntry.getTimestamp().getTime());
290     }
291 
292     public final void file8(String filename) {
293         final File dir = new File(FilenameUtils.acornToNative(filename));
294         boolean success = dir.isDirectory() || dir.mkdir();
295         if (!success) {
296             throw new SWIError(0, "Unable to create directory and it does not already exist");
297         }
298     }
299 
300     public final void file10(String filename, int filetype, ByteArray data) {
301         final File file = new File(FilenameUtils.acornToNative(filename));
302         FileOutputStream outputStream = null;
303         try {
304             outputStream = new FileOutputStream(file);
305             outputStream.write(data.getArray(), data.getOffset(), data.getLength());
306         } catch (IOException e) {
307             throw new SWIError(0, e);
308         } finally {
309             if (outputStream != null) {
310                 try {
311                     outputStream.close();
312                 } catch (IOException e) { // Ignore
313                 }
314             }
315         }
316     }
317 
318     public final void Args(Environment env) {
319         final int[] R = env.getCpu().R;
320         int result = args(R[0], R[1] & 0xff, R[2]);
321         R[2] = result;
322     }
323 
324     public final int args(int reasonCode, int handle, int attribute) {
325         try {
326             switch (reasonCode) {
327                 case 2: // Read extent
328                     RandomAccessFile randomAccessFile = getOpenFile(handle);
329                     return (int) randomAccessFile.length();
330                 default:
331                     throw new SWIError(0, "Unknown OS.Args " + reasonCode);
332             }
333         } catch (IOException e) {
334             throw new SWIError(0, e);
335         }
336     }
337 
338     public final void GBPB(Environment env) {
339         final CPU cpu = env.getCpu();
340         final int[] R = cpu.R;
341         final MemoryMap memoryMap = env.getMemoryMap();
342         switch (R[0]) {
343             case 4:
344                 final int count = R[3];
345                 final byte[] buffer = new byte[count];
346                 int unreadCount = gbpb4(R[0], R[1], buffer, 0, count);
347                 final int readCount = count - unreadCount;
348                 memoryMap.storeBytes(R[2], buffer, 0, readCount);
349                 R[2] += readCount;
350                 R[3] = unreadCount;
351                 if (unreadCount == 0) {
352                     cpu.clearC();
353                 } else {
354                     cpu.setC();
355                 }
356                 log.warning("New file pointer not returned in R4");
357                 break;
358             case 11:
359                 final String dirname = memoryMap.getString0(R[1]);
360                 final String match = R[6] == 0 ? null : memoryMap.getString0(R[6]);
361                 final DirectoryEntry[] entries = gbpb11(R[0], dirname, R[4], R[3], match);
362                 // Store the entries in the buffer
363                 int entryBase = R[2];
364                 R[3] = 0;
365                 for (int i = 0; i < entries.length; i++, R[3]++) {
366                     DirectoryEntry entry = entries[i];
367                     int entrySize = 29 + entry.getName().length() + 1;
368                     if (entryBase + entrySize > R[2] + R[5]) {
369                         break;
370                     }
371                     long fiveByteTime = convertTo5ByteTime(entry.getTimestamp());
372                     memoryMap.storeWords(entryBase, new int[]{entry.getLoadAddress(), entry.getExecAddress(),
373                                                               entry.getLength(), entry.getAttributes(),
374                                                               entry.getObjectType(), entry.getInternalName(),
375                                                               (int) (fiveByteTime & 0xffffffffL)});
376                     memoryMap.storeByte(entryBase + 28, (byte) (fiveByteTime >> 32));
377                     memoryMap.storeString0(entryBase + 29, entry.getName());
378                     entryBase += (entrySize + 3) & (~3); // Word-aligned
379                 }
380                 R[4] = (entries.length == 0) ? -1 : R[4] + entries.length;
381                 if (R[3] == 0) {
382                     cpu.clearC();
383                 } else {
384                     cpu.setC();
385                 }
386                 break;
387             default:
388                 throw new SWIError(0, "Unknown OS.GBPB " + R[0]);
389         }
390     }
391 
392     public final int gbpb4(int reasonCode, int fileHandle, byte[] buffer, int offset, int count) {
393         try {
394             switch (reasonCode) {
395                 case 4: // Read bytes from current file pointer
396                     RandomAccessFile randomAccessFile = getOpenFile(fileHandle);
397                     int readCount = randomAccessFile.read(buffer, offset, count);
398                     if (readCount != -1) {
399                         return count - readCount;
400                     } else {
401                         return count;
402                     }
403                 default:
404                     throw new SWIError(0, "Unknown OS.GBPB " + reasonCode);
405             }
406         } catch (IOException e) {
407             throw new SWIError(0, e);
408         }
409     }
410 
411     private final Map directoryEntryMap = new HashMap(10);
412 
413     public final DirectoryEntry[] gbpb11(int reasonCode, String dirName, int offset, int count, String match) {
414         switch (reasonCode) {
415             case 11:
416                 final String key = dirName + ':' + match;
417                 DirectoryEntry[] entries = (DirectoryEntry[]) directoryEntryMap.get(key);
418                 if (entries == null) {
419                     File f = new File(FilenameUtils.acornToNative(dirName));
420                     if (match != null && !match.equals("*")) {
421                         log.warning("Filename match string ignored");
422                     }
423                     File[] fs = f.listFiles();
424                     entries = new DirectoryEntry[fs == null ? 0 : fs.length];
425                     for (int i = 0; i < entries.length; i++) {
426                         File file = fs[i];
427                         entries[i] = fileDirectoryEntry(file);
428                     }
429                     directoryEntryMap.put(key, entries);
430                 }
431                 if (entries.length - offset < count) {
432                     count = entries.length - offset;
433                 }
434                 DirectoryEntry[] result = new DirectoryEntry[count];
435                 System.arraycopy(entries, offset, result, 0, count);
436                 return result;
437             default:
438                 throw new SWIError(0, "Unknown OS.GBPB " + reasonCode);
439         }
440     }
441 
442     /***
443      * Return a DirectoryEntry for a given File.
444      * DirectoryEntry is a representation of the information RISC OS programs expect to be able to find out about
445      * a file from its directory entry. File is a Java representation of an abstract point in the filesystem.
446      *
447      * @param file filesystem entry to obtain DirectoryEntry for. It need not exist.
448      * @return DirectoryEntry for given file. It will indicate the file's type.
449      */
450     DirectoryEntry fileDirectoryEntry(File file) {
451         long timestamp = file.lastModified();
452         long length = file.length();
453         int attributes = 0;
454         if (file.canRead()) {
455             attributes |= DirectoryEntry.ATTRIBUTE_OWNER_READ + DirectoryEntry.ATTRIBUTE_OTHERS_READ;
456         }
457         if (file.canWrite()) {
458             attributes |= DirectoryEntry.ATTRIBUTE_OWNER_WRITE + DirectoryEntry.ATTRIBUTE_OTHERS_WRITE;
459         }
460         if (file.isHidden()) {
461             attributes |= DirectoryEntry.ATTRIBUTE_HIDDEN + DirectoryEntry.ATTRIBUTE_LOCKED;
462         }
463         final int type = file.exists() ? (file.isDirectory() ? DirectoryEntry.TYPE_DIRECTORY : DirectoryEntry.TYPE_FILE) :
464                 DirectoryEntry.TYPE_NOT_FOUND;
465         return new DirectoryEntry(FilenameUtils.nativeToAcorn(file.getName()),
466                 new Date(timestamp), length, attributes, type);
467     }
468 
469     public final void Find(Environment env) {
470         final int[] R = env.getCpu().R;
471         final int action = R[0] & 0xc0;
472         final int pathType = R[0] & 0x03;
473         boolean dirError = (R[0] & 0x04) != 0;
474         boolean missingError = (R[0] & 0x08) != 0;
475         if (action != 0) {
476             final String filename = env.getMemoryMap().getString0(R[1]);
477             final String path;
478             switch (pathType) {
479                 case 0:
480                     path = readVarVal("File$Path");
481                     break;
482                 case 1:
483                     path = env.getMemoryMap().getString0(R[2]);
484                     break;
485                 case 2:
486                     path = readVarVal(env.getMemoryMap().getString0(R[2]));
487                     break;
488                 default:
489                     path = null;
490             }
491             R[0] = find(action, filename, path, dirError, missingError);
492         } else {
493             find(action, R[1]);
494         }
495     }
496 
497     private static int fileHandleSeq = 100;
498     private final Map randomAccessFileMap = new HashMap(10);
499 
500     public final int find(int action, String filename, String path, boolean dirError, boolean missingError) {
501         if (path != null) {
502             log.warning("We do not support paths in OS.Find");
503         }
504         filename = FilenameUtils.acornToNative(filename);
505         RandomAccessFile randomAccessFile;
506         try {
507             switch (action) {
508                 case 0x40:
509                     // Open existing file with read-only access
510                     randomAccessFile = new RandomAccessFile(filename, "r");
511                     break;
512                 case 0x80:
513                     // Create empty file with read/write access
514                     randomAccessFile = new RandomAccessFile(filename, "rw");
515                     randomAccessFile.setLength(0); // Truncate the file to 0
516                     break;
517                 case 0xc0:
518                     // Open existing file with read/write access
519                     randomAccessFile = new RandomAccessFile(filename, "rw");
520                     break;
521                 default:
522                     throw new SWIError(0, "Unknown OS.Find &" + Integer.toHexString(action));
523             }
524             synchronized (OS.class) {
525                 final int fileHandle = fileHandleSeq++;
526                 randomAccessFileMap.put(new Integer(fileHandle), randomAccessFile);
527                 return fileHandle;
528             }
529         } catch (IOException e) {
530             throw new SWIError(0, e);
531         }
532     }
533 
534     public final int find(int action, int handle) {
535         if (action == 0) {
536             // Close the file with the handle, or close all files if handle = 0
537             RandomAccessFile randomAccessFile = getOpenFile(handle);
538             if (randomAccessFile != null) {
539                 try {
540                     randomAccessFile.close();
541                 } catch (IOException e) {
542                     throw new SWIError(0, e);
543                 }
544             }
545         }
546         return handle;
547     }
548 
549     private RandomAccessFile getOpenFile(int handle) {
550         final RandomAccessFile randomAccessFile = (RandomAccessFile) randomAccessFileMap.get(new Integer(handle));
551         if (randomAccessFile == null) {
552             throw new SWIError(0, "Invalid file handle");
553         }
554         return randomAccessFile;
555     }
556 
557     static final int DEFAULT_RAM_LIMIT = 1024 * 1024;
558     static final AppEnvironment DEFAULT_APP_ENVIRONMENT = new AppEnvironment("ROBE", DEFAULT_RAM_LIMIT, new Date());
559     private RawDataBlock envStartTime = null;
560     private RawDataBlock envCommandLine = null;
561 
562     public final void GetEnv(Environment env) {
563         final int[] R = env.getCpu().R;
564         final MemoryMap memoryMap = env.getMemoryMap();
565 
566         AppEnvironment appEnvironment = getEnv();
567         if (envCommandLine == null) {
568             envCommandLine = memoryMap.createSystemDataBlock(appEnvironment.getCommandLine().length() + 1);
569         }
570         ByteArrayUtils.putString0(envCommandLine.getBytes(), 0, appEnvironment.getCommandLine());
571         R[0] = envCommandLine.getAddress();
572 
573         log.warning("We do not support getting RAM limit correctly, returning size of &" +
574                 Integer.toHexString(appEnvironment.getRamLimit()));
575         R[1] = appEnvironment.getRamLimit();
576 
577         if (envStartTime == null) {
578             envStartTime = memoryMap.createSystemDataBlock(5);
579         }
580         ByteArrayUtils.put5ByteValue(envStartTime.getBytes(), 0, convertTo5ByteTime(appEnvironment.getStartTime()));
581         R[2] = envStartTime.getAddress();
582     }
583 
584     private final static long EPOCH_BASE = new GregorianCalendar(1900, 0, 1, 0, 0, 0).getTimeInMillis();
585 
586     public final static long convertTo5ByteTime(Date date) {
587         long timeMillis = date.getTime(); // milliseconds since January 1, 1970, 00:00:00 GMT
588         long timeCentis = (timeMillis - EPOCH_BASE) / 10; // centiseconds since January 1, 1900, 00:00:00 GMT
589         return timeCentis & 0xffffffffffL;
590     }
591 
592     public final static Date convertToDate(long fiveByteTime) {
593         long timeMillis = (fiveByteTime * 10) + EPOCH_BASE;
594         return new Date(timeMillis);
595     }
596 
597     public final AppEnvironment getEnv() {
598         return DEFAULT_APP_ENVIRONMENT;
599     }
600 
601     public final void Exit(Environment env) {
602         final int[] R = env.getCpu().R;
603         if (R[1] == 0x58454241) { // "ABEX" magic number
604             exit(R[2]);
605         } else {
606             exit(0);
607         }
608     }
609 
610     public final void exit(int rc) {
611         final long executionTime = (System.currentTimeMillis() - OS.DEFAULT_APP_ENVIRONMENT.getStartTime().getTime());
612         log.info("Executed " + Instruction.count + " instructions in " + executionTime + "ms");
613         log.info(" = " + ((double) Instruction.count * 1000 / ((double) executionTime)) + "/s");
614         System.exit(rc);
615     }
616 
617     public final void IntOn(Environment env) {
618         env.getCpu().setI();
619     }
620 
621     public final void IntOff(Environment env) {
622         env.getCpu().clearI();
623     }
624 
625     public final void EnterOS(Environment env) {
626         log.warning("OS.EnterOS ignored");
627     }
628 
629     public final void Module(Environment env) {
630         final int[] R = env.getCpu().R;
631         switch (R[0]) {
632             case 6: // Claim
633                 R[2] = env.getMemoryMap().claimRmaBlock(R[3]).getAddress();
634                 break;
635             case 7: // Free
636                 env.getMemoryMap().releaseRmaBlock(R[2]);
637                 break;
638             case 18: // Lookup module name
639                 final String name = env.getMemoryMap().getString0(R[1]);
640                 if (name.indexOf('%') > -1) {
641                     log.warning("We do not handle modules with multiple instantiations correctly");
642                 }
643                 final Module module = ModuleUtils.getModule(name);
644                 if (module != null) {
645                     R[1] = ModuleUtils.getModuleNumber(module);
646                     R[2] = 0;
647                     R[3] = module.getAddress();
648                     R[4] = 0x98765432; // Private word - use silly value so that we can spot if it's used later
649                     R[5] = 0; // Postfix string
650                 } else {
651                     log.severe("Module not found: " + name);
652                     throw new SWIError(0, "Module not found: " + name);
653                 }
654                 break;
655             default:
656                 throw new SWIError(0, "Unknown OS.Module " + R[0]);
657         }
658     }
659 
660     public final void ReadUnsigned(Environment env) {
661         final int[] R = env.getCpu().R;
662         int base = R[0] & 0xff;
663         if (base < 2 || base > 36) {
664             base = 10;
665         }
666         if ((R[0] & 0x80000000) != 0) {
667             log.warning("we do not support restriction on terminator in OS.ReadUnsigned - ignoring");
668         }
669         final String s = env.getMemoryMap().getStringN(R[1], 34);
670         final int result = readUnsigned(base, s);
671         if (((R[0] & 0x40000000) != 0 && result > 255)
672                 || ((R[0] & 0x2000000) != 0 && result > R[2])) {
673             throw new SWIError(0, "Number out of range");
674         }
675         log.warning("Terminator character pointer is incorrect");
676         R[2] = result;
677     }
678 
679     /***
680      * Convert a String to an integer.
681      * If the String starts with &, base 16 is assumed. If the String starts with <var>base</var>_, this base
682      * is used. If no base is given or the base is invalid, the base in defaultInputBase is used.
683      * The terminating character in the string is the first character that is inconsistent with the base
684      * being used.
685      *
686      * @param defaultInputBase base to use if none is specified in the string
687      * @param input            String to convert
688      * @return
689      */
690     public final int readUnsigned(int defaultInputBase, String input) {
691         int base = defaultInputBase;
692         int underscoreIndex = input.indexOf('_');
693 
694         if (input.startsWith("&")) {
695             base = 16;
696             input = input.substring(1);
697         } else if (underscoreIndex > 0) {
698             final String s = input.substring(0, underscoreIndex);
699             try {
700                 base = Integer.parseInt(s);
701             } catch (NumberFormatException e) {
702                 log.warning("Invalid base " + s + " in OS.readUnsigned");
703             }
704             input = input.substring(underscoreIndex + 1);
705         }
706 
707         if (base < 2 || base > 36) {
708             base = defaultInputBase;
709         }
710 
711         // Find the end of the string
712         for (int i = 0; i < input.length(); i++) {
713             final char c = input.charAt(i);
714             final int decimalDigit = c - '0';
715             final int alphaDigit = Character.toUpperCase(c) - 'A' + 10;
716             if ((decimalDigit < 0 || decimalDigit > 9) &&
717                     (alphaDigit < 10 || alphaDigit >= base)) {
718                 // This character is out of range
719                 input = input.substring(0, i);
720                 break;
721             }
722         }
723         try {
724             return (int) (Long.parseLong(input, base) & 0xffffffffL);
725         } catch (NumberFormatException e) {
726             throw new SWIError(0, e);
727         }
728     }
729 
730     /***
731      * This SWI returns type and value of a variable.
732      * To check if it exists or find its length, call with R2 &lt; 0. If the variable does not exist R2 will be 0.
733      * Otherwise the length is given by NOT(R2), although for anything other than a string variable (type 0) this is the
734      * unexpanded length. For either case an error will be returned.
735      *
736      * @param env IN: R0 = pointer to wildcarded name
737      *            R1 = pointer to buffer
738      *            R2 = length of buffer, or -ve to check existence/read length
739      *            R3 = name pointer, or 0 for 1st call
740      *            R4 = 3 to expand macros and numbers to strings.
741      *            OUT: if R2 was -ve (read length)
742      *            R0 corrupt (pointer to error)
743      *            R2 = NOT (length), or 0 if variable does not exist.
744      *            if R2 was +ve (read value)
745      *            R0 preserved
746      *            R2 = bytes read
747      *            In both cases:
748      *            R3 = new name pointer
749      *            R4 = Variable type
750      */
751     public final void ReadVarVal(Environment env) {
752         final int[] R = env.getCpu().R;
753         final MemoryMap memoryMap = env.getMemoryMap();
754         String value = readVarVal(memoryMap.getString0(R[0]));
755         final int bufferLength = R[2];
756         if (bufferLength < 0) {
757             R[0] = 0;
758             R[2] = value == null ? 0 : ~(value.length());
759         } else {
760             if (bufferLength == 0 || value == null) {
761                 value = "";
762             } else if (value.length() >= bufferLength) {
763                 value = value.substring(0, bufferLength - 1);
764             }
765             memoryMap.storeString0(R[1], value);
766             R[2] = value.length();
767         }
768         R[3] = 0; // todo: new name pointer not returned
769         R[4] = 0; // todo: variable type always fixed
770     }
771 
772     private final static Properties ENVIRONMENT_VARIABLES = new Properties();
773 
774     public final String readVarVal(String varName) {
775         return ENVIRONMENT_VARIABLES.getProperty(varName);
776     }
777 
778     /***
779      * This SWI sets a variable's value to that specified, or deletes the variable. The name may be wildcarded for
780      * deletion and update (using '*' and '#'). Code variables will not be deleted unless R4 = 16. Literal strings
781      * do not need to be null terminated, as R2 is used for the length.
782      *
783      * @param env IN: R0 = pointer to name
784      *            R1 = pointer to value to set to
785      *            R2 = length, or -1 to delete
786      *            R3 = name pointer (0 for 1st call)
787      *            R4 = variable type (0 = string, 1 = integer, 2 = macro, 3 = expression, 4 = literal string, +16 code)
788      *            OUT: R3 = new name pointer
789      *            R4 = variable type
790      */
791     public final void SetVarVal(Environment env) {
792         final int[] R = env.getCpu().R;
793         final MemoryMap memoryMap = env.getMemoryMap();
794         if (R[2] != -1) {
795             setVarVal(memoryMap.getString0(R[0]), memoryMap.getStringN(R[1], R[2]), R[4]);
796         } else {
797             deleteVarVal(memoryMap.getString0(R[0]));
798         }
799     }
800 
801     public void setVarVal(String name, String value, int valueType) {
802         if (valueType != 0 && valueType != 4) {
803             log.warning("setVarVal always stores variables as strings <" + name + "> = \"" + value + "\"");
804         }
805         ENVIRONMENT_VARIABLES.setProperty(name, value);
806     }
807 
808     public void deleteVarVal(String name) {
809         log.warning("setVarVal(deleteVarVal) unimplemented, no value removed for <" + name + ">");
810     }
811 
812     /***
813      * This SWI is equivalent to a call to OS_GSInit, followed by repeated calls    
814      * to OS_GSRead . It reads and translates a whole string.
815      * <pre><![CDATA[
816      * Translates a string as follows:
817      * From  To
818      * |"    "
819      * |<    <
820      * |!    forces top bit of next char to be set.
821      * |char CTRL( ASCII(uppercase(char) - 64 )
822      * "str" str
823      * <num> CTRL(num)
824      * <str> system variable str
825      * ]]>
826      * </pre>
827      *
828      * @param env
829      */
830     public final void GSTrans(Environment env) {
831         final int[] R = env.getCpu().R;
832         String in = env.getMemoryMap().getString0_10_13(R[0]);
833         final boolean spaceTerminates = (R[2] & 0x20000000) != 0;
834         final boolean convertControlCodes = (R[2] & 0x40000000) == 0;
835         final boolean stripQuotes = (R[2] & 0x80000000) == 0;
836         log.warning("convert control codes and strip quotes flags ignored");
837         GSTranslator translator = new GSTranslator(in, spaceTerminates, this);
838         String out = gsTrans(translator, new StringBuffer(in.length()));
839         int bufferLength = R[2] & 0x1fffffff;
840         if (out.length() > bufferLength - 1) {
841             out = out.substring(0, bufferLength - 1);
842             env.getCpu().setC();
843         }
844 
845         R[0] += translator.getIndex() + 1;
846         env.getMemoryMap().storeString0(R[1], out);
847         R[2] = out.length();
848     }
849 
850     public final String gsTrans(String in, boolean spaceTerminates, boolean convertControlCodes, boolean stripQuotes) {
851         StringBuffer out = new StringBuffer(in.length());
852         GSTranslator translator = new GSTranslator(in, spaceTerminates, this);
853 
854         return gsTrans(translator, out);
855     }
856 
857     private String gsTrans(GSTranslator translator, StringBuffer out) {
858         do {
859             char c = translator.nextCharacter();
860             if (c == CharacterIterator.DONE) {
861                 break;
862             } else {
863                 out.append(c);
864             }
865         } while (true);
866         return out.toString();
867     }
868 
869     public final void FSControl(Environment env) {
870         final int[] R = env.getCpu().R;
871         final MemoryMap memoryMap = env.getMemoryMap();
872         String pathname = memoryMap.getString0(R[1]);
873         final String path;
874         if (R[3] != 0) {
875             path = readVarVal(memoryMap.getString0(R[3]));
876         } else if (R[4] != 0) {
877             path = memoryMap.getString0(R[4]);
878         } else {
879             path = null;
880         }
881         pathname = fsControl(R[0], pathname, path);
882         final int bufferLength = R[5];
883         if (pathname.length() > bufferLength) {
884             pathname = pathname.substring(0, bufferLength);
885         }
886         memoryMap.storeString0(R[2], pathname);
887         R[5] -= pathname.length();
888     }
889 
890     public final String fsControl(int reasonCode, String pathname, String path) {
891         if (path != null) {
892             log.warning("Path ignored in OS.FSControl");
893         }
894         try {
895             switch (reasonCode) {
896                 case 37:
897                     File f = new File(FilenameUtils.acornToNative(pathname));
898                     return FilenameUtils.nativeToAcorn(f.getCanonicalPath());
899                 default:
900                     throw new SWIError(0, "Unknown OS.FSControl " + reasonCode);
901             }
902         } catch (IOException e) {
903             throw new SWIError(0, e);
904         }
905     }
906 
907     public final void ChangeDynamicArea(final Environment env) {
908         final int[] R = env.getCpu().R;
909         if (R[0] < 256) {
910             log.warning("We do not support changing non-user areas.");
911         }
912         final DynamicArea dynamicArea = env.getMemoryMap().findDynamicArea(R[0]);
913         final int changeRequest = R[1];
914         if (changeRequest > 0) {
915             dynamicArea.extendForwards(changeRequest);
916         } else {
917             // Ignore shrinkage for now
918             log.info("ChangeDynamicArea will never reduce memory usage!");
919             R[1] = 0;
920         }
921     }
922 
923     public final void GenerateError(Environment env) {
924         final int[] R = env.getCpu().R;
925         throw new SWIError(env, R[0]);
926     }
927 
928     public final void generateError(int errorNumber, String errorMessage) throws SWIError {
929         throw new SWIError(errorNumber, errorMessage);
930     }
931 
932     private static final SpriteArea SYSTEM_SPRITE_AREA = new SpriteArea(0x100000);
933 
934     public final void SpriteOp(Environment env) {
935         final int[] R = env.getCpu().R;
936         final SpriteArea spriteArea;
937         final MemoryMap memoryMap = env.getMemoryMap();
938         final String spriteName = memoryMap.getString0(R[2]);
939         if ((R[0] & 0x300) != 0) {
940             final DataBlock dataBlock = memoryMap.getDataBlock(R[1]);
941             if (!(dataBlock instanceof SpriteArea)) {
942                 spriteArea = new SpriteArea(dataBlock);
943                 memoryMap.replaceDataBlock(dataBlock, spriteArea); // Turn this block into a sprite area.
944             } else {
945                 spriteArea = (SpriteArea) dataBlock;
946             }
947             if ((R[0] & 0x200) != 0) {
948                 log.warning("we do not support sprite pointer arguments to OS.SpriteOp - ignoring");
949                 return;
950             }
951         } else {
952             spriteArea = SYSTEM_SPRITE_AREA;
953         }
954         SpriteOp(R, spriteArea, spriteName);
955     }
956 
957     void SpriteOp(final int[] R, final SpriteArea spriteArea, final String spriteName) {
958         final int operation = R[0] & 0xff;
959         switch (operation) {
960             case 40: // Read sprite info
961                 final BufferedImage image = spriteOp40(spriteArea, spriteName);
962                 R[3] = image.getWidth();
963                 R[4] = image.getHeight();
964                 log.warning("All sprites return as 24bpp with 90x90 dpi since we use TYPE_INT_ARGB native images");
965                 R[5] = image.getColorModel().hasAlpha()? 1 : 0;
966                 R[6] = (6 << 27) | 1 | (90 << 1) | (90 << 14); // 24bpp, 90dpi always
967                 break;
968             default:
969                 spriteOp(operation, spriteArea, spriteName);
970         }
971     }
972 
973     public final BufferedImage spriteOp40(SpriteArea spriteArea, String spriteName) {
974         return spriteArea.getSprite(spriteName).getImage();
975     }
976 
977     public final void spriteOp(int operation, SpriteArea spriteArea, String spriteName) {
978         switch (operation) {
979             case 10: // Load sprites
980                 // spriteName is the filename in this case
981                 synchronized (spriteArea) {
982                     spriteArea.load(new File(FilenameUtils.acornToNative(spriteName)));
983                 }
984                 break;
985             case 11: // Merge sprites
986                 // spriteName is the filename in this case
987                 synchronized (spriteArea) {
988                     spriteArea.merge(new File(FilenameUtils.acornToNative(spriteName)));
989                 }
990                 break;
991             default:
992                 spriteOp(operation, spriteArea, spriteArea.getSprite(spriteName));
993         }
994     }
995 
996     public final void spriteOp(int operation, SpriteArea spriteArea, Sprite sprite) {
997         log.warning("spriteOp on Sprite unimplemented");
998     }
999 
1000     /***
1001      * Entry:
1002      * => R0 = pointer to input block
1003      * R1 = pointer to output block (can be same as R0)
1004      * The input block is a list of variable numbers (words) terminated by -1.
1005      * Each variable is read, and its value is written as a word into the output block.
1006      * Note: You can also read the mode variables with this call.
1007      *
1008      * @param env
1009      */
1010     public final void ReadVduVariables(Environment env) {
1011         final int[] R = env.getCpu().R;
1012         final MemoryMap memoryMap = env.getMemoryMap();
1013         for (int inAddress = R[0], outAddress = R[1]; ; inAddress += 4, outAddress += 4) {
1014             final int number = memoryMap.getWord(inAddress);
1015             if (number == -1) {
1016                 break;
1017             }
1018             final int value = readVduVariable(number);
1019             memoryMap.storeWord(outAddress, value);
1020         }
1021     }
1022 
1023     public final int readVduVariable(int number) {
1024         final DisplayMode displayMode = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDisplayMode();
1025         final int bitDepth = displayMode.getBitDepth();
1026         switch (number) {
1027             case 3:
1028                 if (bitDepth == 24) {
1029                     return -1;
1030                 } else {
1031                     return (1 << bitDepth) - 1;
1032                 }
1033             case 4: // XEigFactor (screenx = osx >> xeig) always returns 1 to make the mode work like VGA
1034                 return 1;
1035             case 5: // YEigFactor (screeny = osy >> xeig) always returns 1 to make the mode work like VGA
1036                 return 1;
1037             case 9: // Log2BPP
1038                 if (bitDepth == -1) {
1039                     return 5;
1040                 } else {
1041                     return log2(bitDepth);
1042                 }
1043             case 11: // XWindLimit (x pixels - 1)
1044                 return displayMode.getWidth() - 1;
1045             case 12: // YWindLimit (y pixels - 1)
1046                 return displayMode.getHeight() - 1;
1047             default:
1048                 log.warning("Cannot read VDU variable " + number);
1049                 return 0;
1050         }
1051     }
1052 
1053     public void ReadModeVariable(Environment env) {
1054         final int[] R = env.getCpu().R;
1055         if (R[0] != -1) {
1056             log.warning("Cannot read mode variable for a different mode: reading for current mode");
1057         }
1058 
1059         if (R[1] < 128) {
1060             R[2] = readVduVariable(R[1]);
1061         } else {
1062             log.info("Attempt to read mode variable " + R[1] + " - failed");
1063             env.getCpu().setC();
1064         }
1065     }
1066 
1067     private int log2(int v) {
1068         for (int log = 0, mask = 1; log < 32; log++, mask <<= 1) {
1069             if ((v & mask) != 0) { // If the bit is set
1070                 if ((v & ~(mask - 1)) == mask) { // If no higher bits are set
1071                     if ((v & ~mask) == mask) { // If no lower bits are set
1072                         return log;
1073                     } else {
1074                         return log + 1;
1075                     }
1076                 }
1077             }
1078         }
1079         throw new IllegalArgumentException("Could not resolve log2(" + v + ")");
1080     }
1081 
1082     public static void main(String[] args) throws UnknownSWIException {
1083         Logger log = Logger.getLogger(OS.class.getName());
1084         Environment environment = new Environment(0x8000, new byte[0]);
1085         final int[] R = environment.getCpu().R;
1086         Instruction swiCall;
1087 
1088         log.info("SWI OS_WriteC");
1089         R[0] = 65;
1090         log.fine("R[0] = " + R[0]);
1091         swiCall = new SWI(Condition.AL, 0x0);
1092         swiCall.checkAndExecute(environment);
1093 
1094         R[0] = 0;
1095         R[1] = 0;
1096         log.fine("R[0] = " + R[0]);
1097         log.fine("R[1] = " + R[1]);
1098         log.info("SWI XOS_Byte");
1099         swiCall = new SWI(Condition.AL, 0x800006);
1100         swiCall.checkAndExecute(environment);
1101         log.fine("=> R[0] = " + R[0]);
1102 
1103         R[0] = 0;
1104         R[1] = 1;
1105         log.fine("R[0] = " + R[0]);
1106         log.fine("R[1] = " + R[1]);
1107         log.info("SWI OS_Byte");
1108         swiCall.checkAndExecute(environment);
1109         log.fine("=> R[1] = " + R[1]);
1110     }
1111 }