DirStream.java

/*
** Module   : DirStream.java
** Abstract : Java implementation of the INPUT FROM OS-DIR Stream
**
** Copyright (c) 2013-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 CS  20130103 Created initial version for Implement OS-DIR support as an INPUT stream task.
** 002 CS  20130325 Changed to handle different behavior from Windows to other OS.
** 003 ECF 20150715 Replace StringBuffer with StringBuilder.
** 004 EVL 20160224 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 005 EVK 20140601 Remove unnecessary NEW LINE from directory list.
** 006 OM  20170205 Added required NEW LINE in directory list for Windows OS.
** 007 OM  20170622 Implemented peekCh() method.
** 008 ECF 20171026 Added write(byte[], int, int) method.
** 009 OM  20210302 Replaced [CharsetConverter] with [Charset] which supports multibyte conversions.
**     SVL 20220129 Fixed readLn() returning the newline separator as a part of the line.
**     EVL 20220325 Base refactoring for Stream based classes getting single byte and array of bytes.
** 010 HC  20230427 Implemented server-side file-system resource. File operations performed on
**                  client can be optionally performed directly on server without the expensive
**                  remote call.
** 011 CA  20230703 The dir 'in' stream must be instance field, as multiple unnamed 'dir streams' can be 
**                  active at a certain point. 
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.util;

import java.io.*;
import java.nio.*;
import java.nio.charset.*;

import com.goldencode.p2j.security.*;
import com.goldencode.util.*;

/**
 * A <code>Stream</code> class implementation for the INPUT FROM OS-DIR 
 * Progress statement.
 * <p>
 * The stream will be initialized with the content of the os-dir parameter
 * each line containing info in this format:
 * <p>
 * <b>&lt;FILE_NAME&gt;  &lt;FILE_PATH&gt;  &lt;FILE_TYPE&gt; </b>
 * <p> 
 * The file type is optional and will not be shown if <code>NO-ATTR-LIST</code>
 * parameter is given to OS-DIR.
 * <p>
 * The implemented file type statuses are:
 * <p>
 * <b>PRIMARY TYPES</b>
 * <ul>
 *    <li><b>F</b> - Regular file.
 *    <li><b>D</b> - Directory.
 *    <li><b>S</b> - Special Device
 *    <li><b>X</b> - Unknown type
 * </ul>
 * <p>
 * <b>SECONDARY (ADDITIVE) TYPES</b>
 * <ul>
 *    <li><b>H</b> - Hidden file.
 *    <li><b>L</b> - Symbolic link.
 *    <li><b>P</b> - Pipe File.
 * </ul>
 * <p>
 * One of the main types will always appear, and can also be accompanied by 
 * one or more of the secondary types.
 * <p>
 * OsDirStream is an Input only Stream therefore some Stream operations will 
 * not be supported. 
 */
public class DirStream
extends Stream
{
   
   /** Error message for unsupported operation*/ 
   private static final String UNSUPPORTED = "Not supported in OS-Dir Stream";
   
   /** Newline separator */
   public static final String NEWLINE = System.getProperty("line.separator");
   
   /** OS file separator */
   private static final String fileSep = System.getProperty("file.separator");
   
   /** The Input stream used to store OS-Dir data */
   private InputStream in = null;

   /**
    * Constructor for the <code>OsDirStream</code>
    * <p>
    * The OsDirStream will be initialized for each content of the
    * given directory including itself(.) and the upper directory (..)
    * with a line containing:
    * <ul>
    *    <li>The file name.
    *    <li>The file path.
    *    <li>The file status if not specified otherwise by noAttrList.
    * </ul>
    *
    * @param   dir
    *          The directory name for which OS-DIR statement is applied
    * @param   noAttrList
    *          This indicates if type index will no be output to stream. If
    *          <code>true</code> the index will not be output to the stream,
    *          if <code>false</code> it will be output
    * @param   fsr
    *          The file system resource to use for this stream. May be {@code null} if no resource
    *          checking is required.
    */
   public DirStream(String dir, boolean noAttrList, FileSystemResource fsr)
   {
      StringBuilder sb = new StringBuilder("");
      File f = new File(dir);

      try
      {
         if (fsr != null && !fsr.canRead(f.getCanonicalPath()))
         {
            String errMsg = "You do not have permission to access " + dir;
            ErrorManager.recordOrThrowError(294, errMsg);
            return;
         }
      }
      catch (IOException e)
      {
         // ignore the IO error at this time, it will be caught later
      }

      // if dir doesn't exist throw error
      if (!f.exists())
      {
         String errMsg = character.quoter(dir).getValue() + " was not found";
         ErrorManager.recordOrThrowError(293, errMsg);
         return;
      }
      
      // if dir is not a directory throw error
      if (!f.isDirectory())
      {
         String errMsg = "Unable to open file: " +   dir + ". Errno=2.";
         ErrorManager.recordOrThrowError(98, errMsg);
         return;
      }
      
      //current and upper dir are also added when we input from os-dir
      //current dir is only added in windows
      if (PlatformHelper.isUnderWindowsFamily())
      {
         File currentDir = new File(dir + fileSep + ".");
         addOsDirLine(currentDir, sb, noAttrList);
         sb.append(NEWLINE);
      }
      
      File upperDir = new File(dir + fileSep + "..");
      addOsDirLine(upperDir, sb, noAttrList);
      
      // get the content of directory
      File[] files = f.listFiles();
      
      if (files != null)
      {
         // for each content of the directory
         for (File file : files)
         {
            sb.append(NEWLINE);
            addOsDirLine(file, sb, noAttrList);
         }
      }
      
      // create the input stream from the StringBuilder variable
      in = new ByteArrayInputStream(sb.toString().getBytes(Charset.defaultCharset()));
   }
   
   /**
    * The number of bytes available to be immediately read without blocking.
    *
    * @return   The number of available bytes.
    * 
    * @throws   IOException
    *           if an I/O error occurs. 
    */
   public long available()
   throws IOException
   {
      return in.available();
   }

   /**
    * The 0-based offset into the stream at which the next read or write will 
    * occur.  This does not work for "streams" that require sequential
    * access.
    *
    * @return   The current position in the stream.
    * 
    * @throws   UnsupportedOperationException 
    *           If the requested operation is not supported.
    * @throws   IOException
    *           If an I/O error occurs. 
    */
   public long getPos()
   throws UnsupportedOperationException,
          IOException
   {
      throw new UnsupportedOperationException(UNSUPPORTED);
   }

   /**
    * Moves the current read/write position to the specified absolute 0-based
    * offset.  A negative offset is ignored (no action is taken).  If the   
    * specified offset is larger than the length of the stream, the seek
    * position will report as the number requested, but if no subsequent
    * writes occur to the file, the file is truncated to 0 bytes.  If writes
    * do occur, all writes occur at byte 0 BUT the file actually is of a size
    * that is the requested offset + the number of bytes written!
    * <p>
    * This does not work for "streams" that require sequential access. 
    *
    * @param    pos
    *           The new read/write position in the stream.
    * 
    * @throws   UnsupportedOperationException 
    *           If the requested operation is not supported.
    * @throws   IOException
    *           If an I/O error occurs. 
    */
   public void setPos(long pos)
   throws UnsupportedOperationException,
          IOException
   {
      throw new UnsupportedOperationException(UNSUPPORTED);
   }

   /**
    * The length of the stream in bytes.  This does not work for "streams"
    * that require sequential access.
    *
    * @return   The length of the stream.
    * 
    * @throws   UnsupportedOperationException 
    *           If the requested operation is not supported.
    * @throws   IOException
    *           If an I/O error occurs. 
    */
   public long getLen()
   throws UnsupportedOperationException,
          IOException
   {
      throw new UnsupportedOperationException(UNSUPPORTED);
   }

   /**
    * Truncates or extends the stream to the specified length if this stream
    * supports such an operation. If truncation is requested, all data
    * located after this point in the file is discarded.  If extending the
    * file is requested, the values of the data in the extended portion of
    * the file is undefined. 
    *
    * @param    len
    *           The new length of the file.
    * 
    * @throws   UnsupportedOperationException 
    *           If the requested operation is not supported.
    * @throws   IOException
    *           If an I/O error occurs. 
    */
   public void setLen(long len)
   throws UnsupportedOperationException,
          IOException
   {
      throw new UnsupportedOperationException(UNSUPPORTED);
   }

   /**
    * Write the given character to the output stream.
    *
    * @param    ch
    *           The character to be written.
    * 
    * @throws   IOException
    *           If an I/O error occurs. 
    */
   public void writeCh(char ch)
   throws IOException
   {
      // protectWrites should handle the error
      if (protectWrites())
         return;
   }

   /**
    * Write the given byte to the output stream.
    *
    * @param    b
    *           The byte to be written.
    * 
    * @throws   IOException
    *           If an I/O error occurs. 
    */
   public void writeByte(byte b)
   throws IOException
   {
      // protectWrites should handle the error
      if (protectWrites())
         return;
   }

   /**
    * Write the given string to the output stream.
    *
    * @param    data
    *           The data to be written.
    * 
    * @throws   IOException
    *           If an I/O error occurs. 
    */
   public void write(String data)
   throws IOException
   {
      // protectWrites should handle the error
      if (protectWrites())
      {
          return;
      }
   }
   
   /**
    * Write the given byte array to the output stream.
    *
    * @param    data
    *           The data to be written.
    *
    * @throws   IOException
    *           If an I/O error occurs.
    */
   public void write(byte[] data)
   throws IOException
   {
      // protectWrites should handle the error
      if (protectWrites())
      {
         return;
      }
   }
   
   /**
    * Write the specified range of bytes from the given byte array to the output stream.
    *
    * @param    data
    *           The data to be written.
    * @param    off
    *           Starting offset in data from which to read bytes to be written. Must be
    *           non-negative and {@code &lt; data.length}.
    * @param    len
    *           Length of data to be written. Must be non-negative and {@code &lt;= (data.length
    *           - offset)}.
    *
    * @throws   IOException
    *           If an I/O error occurs.
    */
   public void write(byte[] data, int off, int len)
   throws IOException
   {
      // protectWrites should handle the error
      if (protectWrites())
      {
         return;
      }
   }
   
   /**
    * Peeks at the character from the current read position in the stream (reads a character from
    * the current read position in the stream without incrementing stream read position. The next
    * {@code peekCh()} and {@code readCh()} will return the same value).
    * <p>
    * The underlying stream subclass determines the content of the result. Byte oriented streams
    * such as pipes or files will return a byte while streams that generate keystrokes or
    * characters may return a DBCS or Unicode character.
    *
    * @return  The next character read from the stream, -1 on any failure and -2 upon an {@code EOF}.
    */
   public int peekCh()
   {
      try
      {
         in.mark(2);
         int ch = in.read();
         in.reset();
         return ch;
      }
      catch (EOFException e)
      {
         return -2; // mark as EOF
      }
      catch (IOException e)
      {
         return -1;
      }
   }
   
   /**
    * Read a character from the current read position in the stream.
    * <p>
    * The underlying stream subclass determines the content of the result. 
    * Byte oriented streams such as pipes or files will return a byte while
    * streams that generate keystrokes or characters may return a DBCS or
    * Unicode character.
    *
    * @return   The next character read from the stream, -1 on any failure
    *           and -2 upon an <code>EOF</code>.
    */
   public int readCh()
   {
      return readByte();
   }
   
   /**
    * Read a single byte from the underlying stream.
    * 
    * @return   A single byte from the stream, -1 on any failure and -2 upon an {@code EOF}.
    */
   @Override
   public int readByte()
   {
      try
      {
         return in.read();
      }
      catch (EOFException e)
      {
         return -2; // mark as EOF
      }
      catch (IOException e)
      {
         return -1;
      }
   }

   /**
    * Read all characters from the current read position in the stream to the
    * next line separator (as determined by the <code>File.separator</code>
    * or to the <code>EOF</code>. Any line separator character(s) and the
    * <code>EOF</code> character are not returned.
    *
    * @return   The next line read from the stream.
    * 
    * @throws   IOException
    *           If an I/O error occurs. 
    * @throws   EOFException 
    *           If this input stream reaches the end before reading all the 
    *           bytes.
    * @throws   InterruptedException
    *           If any thread interrupted the current thread before or while 
    *           the current thread was waiting for a notification. 
    */
   public String readLn()
   throws EOFException,
          IOException,
          InterruptedException
   {
      StringBuilder sb = new StringBuilder();

      while (true)
      {
         int ch = read();

         if (ch == -1)
         {
            // EOF
            break;
         }

         sb.append((char) ch);

         int newLineIndex = sb.indexOf(NEWLINE);
         if (newLineIndex >= 0)
         {
            sb.delete(newLineIndex, sb.length());
            break;
         }
      }
      if (sb.length() == 0)
      {
         throw new EOFException();
      }
      return sb.toString();
   }

   /**
    * Closes the input stream and releases OS resources associated with it.
    */
   public void closeIn()
   {
      close();
   }

   /**
    * This just redirects to @{link #close} since this stream type cannot be
    * an output stream.
    */
   public void closeOut()
   {
      close();
   }

   /**
    * Close the input side of the stream and release OS resources.
    */
   public void close()
   {
      try
      {
         in.close();
      }
      catch (IOException e)
      {
         // ignore
      }
   }

   /**
    * State of the input side of the stream.
    *
    * @return   <code>true</code> if the input side of the stream is active.
    */
   public boolean isIn()
   {
      return true;
   }

   /**
    * State of the output side of the stream.
    *
    * @return   <code>true</code> if the output side of the stream is active.
    */
   public boolean isOut()
   {
      return false;
   }

   /**
    * Assigns the internal stream reference to the given reference.
    *
    * @param    stream
    *           The new internal stream reference to use for all operations.
    *           
    * @throws   UnsupportedOperationException 
    *           If the requested operation is not supported.
    */
   public void assign(Stream stream)
   throws UnsupportedOperationException
   {
      throw new UnsupportedOperationException("Cannot handle this.");
   }
   
   /**
    * Worker to read a character and hide any charset conversion that may or may not be needed.
    * 
    * @return The next character read or -1 if EOF is reached.
    */
   private int read()
   {
      if (true)
      {
         return readCh();
      }
      
// the next code is temporarily disabled
      final int MAX_BYTES = 10; // max of bytes per character in encoded text
      ByteBuffer charBytes = ByteBuffer.allocate(MAX_BYTES);
      for (int i = 0; i < MAX_BYTES; i++)
      {
         in.mark(1);
         int c = readCh();
         if (c < 0)
         {
            // even if some bytes were read, there were not enough to be able to decode a character
            return c;
         }
         
         charBytes.put((byte) (c & 0x0FF));
         CharBuffer chars = sourceCharset.decode(charBytes);
         if (chars.position() != 1)
         {
            // read more than a char?
            try
            {
               in.reset();
            }
            catch (IOException e)
            {
               // no exception expected
            }
            
            return chars.get();
         }
         char ch = chars.get();
         if (ch != '\uFFFD') // REPLACEMENT CHARACTER ?
         {
            return ch;
         }
      }
      
      return '\uFFFD'; // REPLACEMENT CHARACTER ?
   }
   
   /**
    * Utility methods that takes a <code>File</code> input and appends to the
    * given <code>StringBuilder</code> parameter: 
    * <ul>
    *    <li>The file name.
    *    <li>The file path.
    *    <li>The file status if not specified otherwise by noAttrList.
    * </ul>
    * <p>
    * 
    * The file type is obtained by calling FileChecker.getFileType() which 
    * will execute native code to get OS specific file type.
    * 
    * @param file
    *        The <code>File</code> for which the line information will be 
    *        appended to the <code>StringBuilder variable</code>.
    * @param sb
    *        The <code>StringBuilder</code> variable to which the information 
    *        will be appended.
    * @param noAttrList
    *        This indicates if type index will no be output to stream. If
    *        <code>true</code> the index will not be output to the stream,
    *        if <code>false</code> it will be output
    */
   private void addOsDirLine(File file , StringBuilder sb, boolean noAttrList)
   {
      sb.append(character.quoter(file.getName()).toStringMessage()).append(' ')
        .append(character.quoter(getPathName(file)).toStringMessage());
      if (!noAttrList)
      {
         sb.append(' ').append(FileChecker.getFileType(getPathName(file)));
      }
   }
   
   /**
    * Utility method that gets the absolute path name from a <code>File</code> 
    * which may be given with a relative path. If current "." and upper ".." 
    * directory  are included they will be added to the found path be added to
    * the canonical path.
    *
    * @param  file
    *         The <code>File</code> for which we want to return the absolute 
    *         path.
    *
    * @return The absolute path of the given file for Progress format.
    */
   private String getPathName(File file)
   {
      String pathName = "";
      try
      {
         // Build the pathname from the canonicalPath of the parent file/directory plus filename
         // because in a link file case the canonicalPath will point to the file/directory
         // to which the link is pointing to.
         pathName = new File(file.getParentFile().getCanonicalPath(),
                             file.getName()).getAbsolutePath();
      }
      catch (IOException e)
      {
         // If canonical path fails fallback on absolute path?
         pathName = file.getAbsolutePath();
      }
      return pathName;
   }
}