chromium/ui/file_manager/file_manager/background/js/crostini.ts

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Handles shares for Crostini VMs.
 */

import {assert} from 'chrome://resources/ash/common/assert.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';

import type {VolumeManager} from '../../background/js/volume_manager.js';
import type {FilesAppEntry} from '../../common/js//files_app_entry_types.js';
import {RootType} from '../../common/js/volume_manager_types.js';

/**
 * Default Crostini VM is 'termina'.
 */
const DEFAULT_VM = 'termina';

/**
 * Plugin VM 'PvmDefault'.
 */
const PLUGIN_VM = 'PvmDefault';

/**
 * Valid root types to their share location.
 */
const VALID_ROOT_TYPES_FOR_SHARE = new Map<RootType, string>([
  [RootType.DOWNLOADS, 'Downloads'],
  [RootType.REMOVABLE, 'Removable'],
  [RootType.ANDROID_FILES, 'AndroidFiles'],
  [RootType.COMPUTERS_GRAND_ROOT, 'DriveComputers'],
  [RootType.COMPUTER, 'DriveComputers'],
  [RootType.DRIVE, 'MyDrive'],
  [RootType.SHARED_DRIVES_GRAND_ROOT, 'TeamDrive'],
  [RootType.SHARED_DRIVE, 'TeamDrive'],
  [RootType.DRIVE_SHARED_WITH_ME, 'SharedWithMe'],
  [RootType.CROSTINI, 'Crostini'],
  [RootType.GUEST_OS, 'GuestOs'],
  [RootType.ARCHIVE, 'Archive'],
  [RootType.SMB, 'SMB'],
]);

/**
 * Implementation of Crostini shared path state handler.
 */
export class Crostini {
  /**
   * Keys maintaining enablement state for VMs that is keyed by vm then subkeyed
   * by the container name.
   */
  private enabled_: {[key: string]: {[key: string]: boolean}} = {};

  /**
   * A list of shared paths keyed by the VM name.
   */
  private sharedPaths_: {[key: string]: string[]} = {};

  /**
   * The volume manager instance.
   */
  private volumeManager_: VolumeManager|null = null;

  /**
   * Initialize enabled settings and register for any shared path changes.
   * Must be done after loadTimeData is available.
   */
  initEnabled() {
    const guests = loadTimeData.getValue('VMS_FOR_SHARING');
    for (const guest of guests) {
      this.setEnabled(guest.vmName, guest.containerName, true);
    }
    chrome.fileManagerPrivate.onCrostiniChanged.addListener(
        this.onCrostiniChanged_.bind(this));
  }

  /**
   * Initialize Volume Manager.
   */
  initVolumeManager(volumeManager: VolumeManager) {
    this.volumeManager_ = volumeManager;
  }

  /**
   * Set whether the specified Guest is enabled.
   */
  setEnabled(vmName: string, containerName: string, enabled: boolean) {
    if (!this.enabled_[vmName]) {
      this.enabled_[vmName] = {};
    }
    this.enabled_[vmName]![containerName] = enabled;
  }

  /**
   * Returns true if the specified VM is enabled.
   */
  isEnabled(vmName: string) {
    return (!!this.enabled_[vmName]) &&
        Object.values(this.enabled_[vmName]!).includes(true);
  }

  /**
   * Get the root type for the supplied `entry`.
   */
  private getRoot_(entry: Entry|FilesAppEntry) {
    const info =
        this.volumeManager_ && this.volumeManager_.getLocationInfo(entry);
    return info && info.rootType;
  }

  /**
   * Registers an entry as a shared path for the specified VM.
   */
  registerSharedPath(vmName: string, entry: Entry) {
    const url = entry.toURL();
    // Remove any existing paths that are children of the new path.
    // These paths will still be shared as a result of a parent path being
    // shared, but if the parent is unshared in the future, these children
    // paths should not remain.
    for (const [path, _] of Object.entries(this.sharedPaths_)) {
      if (path.startsWith(url)) {
        this.unregisterSharedPath_(vmName, path);
      }
    }
    if (this.sharedPaths_[url]) {
      this.sharedPaths_[url]!.push(vmName);
    } else {
      this.sharedPaths_[url] = [vmName];
    }
  }

  /**
   * Unregisters path as a shared path from the specified VM.
   */
  private unregisterSharedPath_(vmName: string, path: string) {
    const vms = this.sharedPaths_[path];
    if (vms) {
      const newVms = vms.filter(vm => vm !== vmName);
      if (newVms.length > 0) {
        this.sharedPaths_[path] = newVms;
      } else {
        delete this.sharedPaths_[path];
      }
    }
  }

  /**
   * Unregisters entry as a shared path from the specified VM.
   */
  unregisterSharedPath(vmName: string, entry: Entry) {
    this.unregisterSharedPath_(vmName, entry.toURL());
  }

  /**
   * Handles events for enable/disable, share/unshare.
   */
  private onCrostiniChanged_(event: chrome.fileManagerPrivate.CrostiniEvent) {
    const CrostiniEventType = chrome.fileManagerPrivate.CrostiniEventType;
    switch (event.eventType) {
      case CrostiniEventType.ENABLE:
        this.setEnabled(event.vmName, event.containerName, true);
        break;
      case CrostiniEventType.DISABLE:
        this.setEnabled(event.vmName, event.containerName, false);
        break;
      case CrostiniEventType.SHARE:
        for (const entry of event.entries) {
          this.registerSharedPath(event.vmName, assert(entry));
        }
        break;
      case CrostiniEventType.UNSHARE:
        for (const entry of event.entries) {
          this.unregisterSharedPath(event.vmName, assert(entry));
        }
        break;
    }
  }

  /**
   * Returns true if entry is shared with the specified VM. Returns true if path
   * is shared either by a direct share or from one of its ancestor directories.
   */
  isPathShared(vmName: string, entry: Entry|FilesAppEntry) {
    // Check path and all ancestor directories.
    let path = entry.toURL();
    let root = path;
    if (entry && entry.filesystem && entry.filesystem.root) {
      root = entry.filesystem.root.toURL();
    }

    while (path.length > root.length) {
      const vms = this.sharedPaths_[path];
      if (vms && vms.includes(vmName)) {
        return true;
      }
      path = path.substring(0, path.lastIndexOf('/'));
    }
    const rootVms = this.sharedPaths_[root];
    return !!rootVms && rootVms.includes(vmName);
  }

  /**
   * Returns true if entry can be shared with the specified VM.
   */
  canSharePath(vmName: string, entry: Entry|FilesAppEntry, persist: boolean):
      boolean {
    if (!this.isEnabled(vmName)) {
      return false;
    }

    // Only directories for persistent shares.
    if (persist && !entry.isDirectory) {
      return false;
    }

    const root = this.getRoot_(entry);

    // TODO(crbug.com/40607763): Remove when DriveFS enforces allowed write paths.
    // Disallow Computers Grand Root, and Computer Root.
    if (root === RootType.COMPUTERS_GRAND_ROOT ||
        (root === RootType.COMPUTER && entry.fullPath.split('/').length <= 3)) {
      return false;
    }

    // TODO(crbug.com/41456343): Sharing Play files root is disallowed until
    // we can ensure it will not also share Downloads.
    if (root === RootType.ANDROID_FILES && entry.fullPath === '/') {
      return false;
    }

    // Special case to disallow PluginVm sharing on /MyFiles/PluginVm and
    // subfolders since it gets shared by default.
    if (vmName === PLUGIN_VM && root === RootType.DOWNLOADS &&
        entry.fullPath.split('/')[1] === PLUGIN_VM) {
      return false;
    }

    // Disallow sharing LinuxFiles with itself.
    if (vmName === DEFAULT_VM && root === RootType.CROSTINI) {
      return false;
    }

    // Cannot share root of Shared with me since it represents 2 dirs:
    // `.files-by-id` and `.shortcut-targets-by-id`.
    if (root === RootType.DRIVE_SHARED_WITH_ME && entry.fullPath === '/') {
      return false;
    }

    return !!root && VALID_ROOT_TYPES_FOR_SHARE.has(root);
  }
}