markdown pdfを魔改造した

利点

vscode拡張機能のコアファイルは保存場所が既に決定されており,それが使うパスの書き換えは表からはほぼできない
なので内部のスクリプトを弄ってmarkdown pdf関係の書類をクラウドなどで共有できるようにした,これならコピペ不要
さらにどの場所でmarkdown pdfをやっても必ず同じ結果が得られるようになる,ただしvscodeの環境設定で弄れる範囲を除く

欠点

vscodeの環境設定にパラメータを追加できるのは開発者だけらしく大元ディレクトリのフルパスとプラグインのリストアップはextension.jsに書き込む必要があり面倒

概要

そこそこに改変した
まずmarkdown pdf拡張機能のコアファイルのextension.js(ここの最後の方でいじってたやつ)を以下でコピペして上書きする,だいたい1050行くらい
次にmyGroundPathとmyPluginListをその冒頭箇所に指示通りに書き込む,Windowsではエスケープ処理を忘れないように
更に関係ファイルはmyGroundPathの配下に(例として)以下のように設置する,プラグインは同名のcss・html・jsをそれぞれ必ず配置してmyPluginListとの対応を必ず確保しておく
最後にbase.htmlも間違えずにコピペする,コレで終わり

.
├── base
│   └── base.html
├── plugins
│   └── math
│       ├── math.css
│       ├── math.html
│       └── math.js
├── template
│   ├── template.css
│   ├── template.html
│   └── template.js
└── test
    ├── test.html
    └── test.md

ここのmathとはmarkdown pdf上でkatexを動かすのに必要な設定やマクロなどを集めたもので,導入スクリプトと設定スクリプトで分けてある
そっちの周りのお話はここを参考にしてほしい

ソース

以下はbase.html

<!DOCTYPE html>
<html>
<head>
<title>{{{title}}}</title>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
{{{frame}}}
{{{style}}}
{{{script}}}
{{{mermaid}}}
<script>
    // insert your test script!
    console.log("rendered from base.html!");
</script>
</head>
<body>
  <script>
    mermaid.initialize({
      startOnLoad: true,
      theme: document.body.classList.contains('vscode-dark') || document.body.classList.contains('vscode-high-contrast')
          ? 'dark'
          : 'default'
    });
  </script>
{{{content}}}
</body>
</html>

ここの{{{example}}}などに中身を突っ込むためのテンプレートエンジンはmustache.js,と言うよりextension.jsも含め拡張機能全体がnodeで動作しているっぽい,それはそうか

以下はextension.js

'use strict';
var vscode = require('vscode');
var path = require('path');
var fs = require('fs');
var url = require('url');
var os = require('os');
const { least } = require('d3-array');
var INSTALL_CHECK = false;

// set mother directory path for entire files as String.
const myGroundPath="/hogehoge/hugahuga/markdownPdf";
// set list of extensions as List[String,String...].
// you should place files like
//
// markdownPdf
// ├ foo
// │  ├ foo.html
// │  ├ foo.js
// │  ├ foo.css   
// ├ bar
// │  ├ bar.html
// .  ...
//
//.
const myPluginList=["foo","bar"];



function activate(context) {
  init();

  var commands = [
    vscode.commands.registerCommand('extension.markdown-pdf.settings', async function () { await markdownPdf('settings'); }),
    vscode.commands.registerCommand('extension.markdown-pdf.pdf', async function () { await markdownPdf('pdf'); }),
    vscode.commands.registerCommand('extension.markdown-pdf.html', async function () { await markdownPdf('html'); }),
    vscode.commands.registerCommand('extension.markdown-pdf.png', async function () { await markdownPdf('png'); }),
    vscode.commands.registerCommand('extension.markdown-pdf.jpeg', async function () { await markdownPdf('jpeg'); }),
    vscode.commands.registerCommand('extension.markdown-pdf.all', async function () { await markdownPdf('all'); })
  ];
  commands.forEach(function (command) {
    context.subscriptions.push(command);
  });

  var isConvertOnSave = vscode.workspace.getConfiguration('markdown-pdf')['convertOnSave'];
  if (isConvertOnSave) {
    var disposable_onsave = vscode.workspace.onDidSaveTextDocument(function () { markdownPdfOnSave(); });
    context.subscriptions.push(disposable_onsave);
  }
}
exports.activate = activate;


// this method is called when your extension is deactivated
function deactivate() {
}
exports.deactivate = deactivate;

async function markdownPdf(option_type) {

  try {

    // check active window
    var editor = vscode.window.activeTextEditor;
    if (!editor) {
      vscode.window.showWarningMessage('No active Editor!');
      return;
    }

    // check markdown mode
    var mode = editor.document.languageId;
    if (mode != 'markdown') {
      vscode.window.showWarningMessage('It is not a markdown mode!');
      return;
    }

    var uri = editor.document.uri;
    var mdfilename = uri.fsPath;
    var ext = path.extname(mdfilename);
    if (!isExistsPath(mdfilename)) {
      if (editor.document.isUntitled) {
        vscode.window.showWarningMessage('Please save the file!');
        return;
      }
      vscode.window.showWarningMessage('File name does not get!');
      return;
    }

    var types_format = ['html', 'pdf', 'png', 'jpeg'];
    var filename = '';
    var types = [];
    if (types_format.indexOf(option_type) >= 0) {
      types[0] = option_type;
    } else if (option_type === 'settings') {
      var types_tmp = vscode.workspace.getConfiguration('markdown-pdf')['type'] || 'pdf';
      if (types_tmp && !Array.isArray(types_tmp)) {
          types[0] = types_tmp;
      } else {
        types = vscode.workspace.getConfiguration('markdown-pdf')['type'] || 'pdf';
      }
    } else if (option_type === 'all') {
      types = types_format;
    } else {
      showErrorMessage('markdownPdf().1 Supported formats: html, pdf, png, jpeg.');
      return;
    }

    // convert and export markdown to pdf, html, png, jpeg
    if (types && Array.isArray(types) && types.length > 0) {
      for (var i = 0; i < types.length; i++) {
        var type = types[i];
        if (types_format.indexOf(type) >= 0) {
          filename = mdfilename.replace(ext, '.' + type);
          var text = editor.document.getText();
          var content = convertMarkdownToHtml(mdfilename, type, text);
          var html = makeHtml(content, uri);
          await exportPdf(html, filename, type, uri);
        } else {
          showErrorMessage('markdownPdf().2 Supported formats: html, pdf, png, jpeg.');
          return;
        }
      }
    } else {
      showErrorMessage('markdownPdf().3 Supported formats: html, pdf, png, jpeg.');
      return;
    }
  } catch (error) {
    showErrorMessage('markdownPdf()', error);
  }
}

function markdownPdfOnSave() {
  try {
    var editor = vscode.window.activeTextEditor;
    var mode = editor.document.languageId;
    if (mode != 'markdown') {
      return;
    }
    if (!isMarkdownPdfOnSaveExclude()) {
      markdownPdf('settings');
    }
  } catch (error) {
    showErrorMessage('markdownPdfOnSave()', error);
  }
}


function isMarkdownPdfOnSaveExclude() {
  try{
    var editor = vscode.window.activeTextEditor;
    var filename = path.basename(editor.document.fileName);
    var patterns = vscode.workspace.getConfiguration('markdown-pdf')['convertOnSaveExclude'] || '';
    var pattern;
    var i;
    if (patterns && Array.isArray(patterns) && patterns.length > 0) {
      for (i = 0; i < patterns.length; i++) {
        pattern = patterns[i];
        var re = new RegExp(pattern);
        if (re.test(filename)) {
          return true;
        }
      }
    }
    return false;
  } catch (error) {
    showErrorMessage('isMarkdownPdfOnSaveExclude()', error);
  }
}


/*
 * convert markdown to html (markdown-it)
 */
function convertMarkdownToHtml(filename, type, text) {
  var grayMatter = require("gray-matter");
  var matterParts = grayMatter(text);

  try {
    try {
      var statusbarmessage = vscode.window.setStatusBarMessage('$(markdown) Converting (convertMarkdownToHtml) ...');
      var hljs = require('highlight.js');
      var breaks = setBooleanValue(matterParts.data.breaks, vscode.workspace.getConfiguration('markdown-pdf')['breaks']);
      var md = require('markdown-it')({
        html: true,
        breaks: breaks,
        highlight: function (str, lang) {

          if (lang && lang.match(/\bmermaid\b/i)) {
            return `<div class="mermaid">${str}</div>`;
          }

          if (lang && hljs.getLanguage(lang)) {
            try {
              str = hljs.highlight(lang, str, true).value;
            } catch (error) {
              str = md.utils.escapeHtml(str);

              showErrorMessage('markdown-it:highlight', error);
            }
          } else {
            str = md.utils.escapeHtml(str);
          }
          return '<pre class="hljs"><code><div>' + str + '</div></code></pre>';
        }
      });
    } catch (error) {
      statusbarmessage.dispose();
      showErrorMessage('require(\'markdown-it\')', error);
    }

  // convert the img src of the markdown
  var cheerio = require('cheerio');
  var defaultRender = md.renderer.rules.image;
  md.renderer.rules.image = function (tokens, idx, options, env, self) {
    var token = tokens[idx];
    var href = token.attrs[token.attrIndex('src')][1];
    // console.log("original href: " + href);
    if (type === 'html') {
      href = decodeURIComponent(href).replace(/("|')/g, '');
    } else {
      href = convertImgPath(href, filename);
    }
    // console.log("converted href: " + href);
    token.attrs[token.attrIndex('src')][1] = href;
    // // pass token to default renderer.
    return defaultRender(tokens, idx, options, env, self);
  };

  if (type !== 'html') {
    // convert the img src of the html
    md.renderer.rules.html_block = function (tokens, idx) {
      var html = tokens[idx].content;
      var $ = cheerio.load(html);
      $('img').each(function () {
        var src = $(this).attr('src');
        var href = convertImgPath(src, filename);
        $(this).attr('src', href);
      });
      return $.html();
    };
  }

  // checkbox
  md.use(require('markdown-it-checkbox'));

  // emoji
  var emoji_f = setBooleanValue(matterParts.data.emoji, vscode.workspace.getConfiguration('markdown-pdf')['emoji']);
  if (emoji_f) {
    var emojies_defs = require(path.join(__dirname, 'data', 'emoji.json'));
    try {
      var options = {
        defs: emojies_defs
      };
    } catch (error) {
      statusbarmessage.dispose();
      showErrorMessage('markdown-it-emoji:options', error);
    }
    md.use(require('markdown-it-emoji'), options);
    md.renderer.rules.emoji = function (token, idx) {
      var emoji = token[idx].markup;
      var emojipath = path.join(__dirname, 'node_modules', 'emoji-images', 'pngs', emoji + '.png');
      var emojidata = readFile(emojipath, null).toString('base64');
      if (emojidata) {
        return '<img class="emoji" alt="' + emoji + '" src="data:image/png;base64,' + emojidata + '" />';
      } else {
        return ':' + emoji + ':';
      }
    };
  }

  // toc
  // https://github.com/leff/markdown-it-named-headers
  var options = {
    slugify: Slug
  }
  md.use(require('markdown-it-named-headers'), options);

  // markdown-it-container
  // https://github.com/markdown-it/markdown-it-container
  md.use(require('markdown-it-container'), '', {
    validate: function (name) {
      return name.trim().length;
    },
    render: function (tokens, idx) {
      if (tokens[idx].info.trim() !== '') {
        return `<div class="${tokens[idx].info.trim()}">\n`;
      } else {
        return `</div>\n`;
      }
    }
  });

  // PlantUML
  // https://github.com/gmunguia/markdown-it-plantuml
  var plantumlOptions = {
    openMarker: matterParts.data.plantumlOpenMarker || vscode.workspace.getConfiguration('markdown-pdf')['plantumlOpenMarker'] || '@startuml',
    closeMarker: matterParts.data.plantumlCloseMarker || vscode.workspace.getConfiguration('markdown-pdf')['plantumlCloseMarker'] || '@enduml',
    server: vscode.workspace.getConfiguration('markdown-pdf')['plantumlServer'] || ''
  }
  md.use(require('markdown-it-plantuml'), plantumlOptions);

  // markdown-it-include
  // https://github.com/camelaissani/markdown-it-include
  // the syntax is :[alt-text](relative-path-to-file.md)
  // https://talk.commonmark.org/t/transclusion-or-including-sub-documents-for-reuse/270/13
  if (vscode.workspace.getConfiguration('markdown-pdf')['markdown-it-include']['enable']) {
    md.use(require("markdown-it-include"), {
      root: path.dirname(filename),
      includeRe: /:\[.+\]\((.+\..+)\)/i
    });
  }

  statusbarmessage.dispose();
  return md.render(matterParts.content);

  } catch (error) {
    statusbarmessage.dispose();
    showErrorMessage('convertMarkdownToHtml()', error);
  }
}


/*
 * https://github.com/microsoft/vscode/blob/ca4ceeb87d4ff935c52a7af0671ed9779657e7bd/extensions/markdown-language-features/src/slugify.ts#L26
 */
function Slug(string) {
  try {
    var stg = encodeURI(
      string.trim()
            .toLowerCase()
            .replace(/\s+/g, '-') // Replace whitespace with -
            .replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators
            .replace(/^\-+/, '') // Remove leading -
            .replace(/\-+$/, '') // Remove trailing -
    );
    return stg;
  } catch (error) {
    showErrorMessage('Slug()', error);
  }
}


/*
 * make html
 */
function makeHtml(data, uri) {
  try {

    // get title
    let title = path.basename(uri.fsPath);

    // read base
    let filePath = path.join(myGroundPath,'base','base.html')
    var base = readFile(filePath);

    // read frames
    let frame = '';
    frame += readFrames(uri);

    // read styles
    let style = '';
    style += readStyles(uri);

    // read scripts
    let script = '';
    script += readScripts(uri);

    // read mermaid javascripts
    let mermaidServer = vscode.workspace.getConfiguration('markdown-pdf')['mermaidServer'] || '';
    let mermaid = '<script src=\"' + mermaidServer + '\"></script>';

    // compile html
    let mustache = require('mustache');
    let view = {
      title: title,
      frame: frame,
      style: style,
      script: script,
      content: data,
      mermaid: mermaid
    };
    return mustache.render(base, view);
  } catch (error) {
    showErrorMessage('makeHtml()', error);
  }
}


/*
 * export a html to a html file
 */
function exportHtml(data, filename) {
  fs.writeFile(filename, data, 'utf-8', function (error) {
    if (error) {
      showErrorMessage('exportHtml()', error);
      return;
    }
  });
}


/*
 * export a html to a pdf file (html-pdf)
 */
function exportPdf(data, filename, type, uri) {

  if (!INSTALL_CHECK) {
    return;
  }
  if (!checkPuppeteerBinary()) {
    showErrorMessage('Chromium or Chrome does not exist! \
      See https://github.com/yzane/vscode-markdown-pdf#install');
    return;
  }

  var StatusbarMessageTimeout = vscode.workspace.getConfiguration('markdown-pdf')['StatusbarMessageTimeout'];
  vscode.window.setStatusBarMessage('');
  var exportFilename = getOutputDir(filename, uri);

  return vscode.window.withProgress({
    location: vscode.ProgressLocation.Notification,
    title: '[Markdown PDF]: Exporting (' + type + ') ...'
    }, async () => {
      try {
        // export html
        if (type == 'html') {
          exportHtml(data, exportFilename);
          vscode.window.setStatusBarMessage('$(markdown) ' + exportFilename, StatusbarMessageTimeout);
          return;
        }

        const puppeteer = require('puppeteer-core');
        // create temporary file
        var f = path.parse(filename);
        var tmpfilename = path.join(f.dir, f.name + '_tmp.html');
        exportHtml(data, tmpfilename);
        var options = {
          executablePath: vscode.workspace.getConfiguration('markdown-pdf')['executablePath'] || puppeteer.executablePath(),
          args: ['--lang='+vscode.env.language, '--no-sandbox', '--disable-setuid-sandbox']
          // Setting Up Chrome Linux Sandbox
          // https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md#setting-up-chrome-linux-sandbox
      };
        const browser = await puppeteer.launch(options);
        const page = await browser.newPage();
        await page.goto(vscode.Uri.file(tmpfilename).toString(), { waitUntil: 'networkidle0', timeout:0});
        // generate pdf
        // https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagepdfoptions
        if (type == 'pdf') {
          // If width or height option is set, it overrides the format option.
          // In order to set the default value of page size to A4, we changed it from the specification of puppeteer.
          var width_option = vscode.workspace.getConfiguration('markdown-pdf', uri)['width'] || '';
          var height_option = vscode.workspace.getConfiguration('markdown-pdf', uri)['height'] || '';
          var format_option = '';
          if (!width_option && !height_option) {
            format_option = vscode.workspace.getConfiguration('markdown-pdf', uri)['format'] || 'A4';
          }
          var landscape_option;
          if (vscode.workspace.getConfiguration('markdown-pdf', uri)['orientation'] == 'landscape') {
            landscape_option = true;
          } else {
            landscape_option = false;
          }
          var options = {
            path: exportFilename,
            scale: vscode.workspace.getConfiguration('markdown-pdf', uri)['scale'],
            displayHeaderFooter: vscode.workspace.getConfiguration('markdown-pdf', uri)['displayHeaderFooter'],
            headerTemplate: vscode.workspace.getConfiguration('markdown-pdf', uri)['headerTemplate'] || '',
            footerTemplate: vscode.workspace.getConfiguration('markdown-pdf', uri)['footerTemplate'] || '',
            printBackground: vscode.workspace.getConfiguration('markdown-pdf', uri)['printBackground'],
            landscape: landscape_option,
            pageRanges: vscode.workspace.getConfiguration('markdown-pdf', uri)['pageRanges'] || '',
            format: format_option,
            width: vscode.workspace.getConfiguration('markdown-pdf', uri)['width'] || '',
            height: vscode.workspace.getConfiguration('markdown-pdf', uri)['height'] || '',
            margin: {
              top: vscode.workspace.getConfiguration('markdown-pdf', uri)['margin']['top'] || '',
              right: vscode.workspace.getConfiguration('markdown-pdf', uri)['margin']['right'] || '',
              bottom: vscode.workspace.getConfiguration('markdown-pdf', uri)['margin']['bottom'] || '',
              left: vscode.workspace.getConfiguration('markdown-pdf', uri)['margin']['left'] || ''
            }
          }
          await page.pdf(options);
        }

        // generate png and jpeg
        // https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagescreenshotoptions
        if (type == 'png' || type == 'jpeg') {
          // Quality options do not apply to PNG images.
          var quality_option;
          if (type == 'png') {
            quality_option = undefined;
          }
          if (type == 'jpeg') {
            quality_option = vscode.workspace.getConfiguration('markdown-pdf')['quality'] || 100;
          }

          // screenshot size
          var clip_x_option = vscode.workspace.getConfiguration('markdown-pdf')['clip']['x'] || null;
          var clip_y_option = vscode.workspace.getConfiguration('markdown-pdf')['clip']['y'] || null;
          var clip_width_option = vscode.workspace.getConfiguration('markdown-pdf')['clip']['width'] || null;
          var clip_height_option = vscode.workspace.getConfiguration('markdown-pdf')['clip']['height'] || null;
          var options;
          if (clip_x_option !== null && clip_y_option !== null && clip_width_option !== null && clip_height_option !== null) {
            options = {
              path: exportFilename,
              quality: quality_option,
              fullPage: false,
              clip: {
                x: clip_x_option,
                y: clip_y_option,
                width: clip_width_option,
                height: clip_height_option,
              },
              omitBackground: vscode.workspace.getConfiguration('markdown-pdf')['omitBackground'],
            }
          } else {
            options = {
              path: exportFilename,
              quality: quality_option,
              fullPage: true,
              omitBackground: vscode.workspace.getConfiguration('markdown-pdf')['omitBackground'],
            }
          }
          await page.screenshot(options);
        }

        await browser.close();

        // delete temporary file
        var debug = vscode.workspace.getConfiguration('markdown-pdf')['debug'] || false;
        if (!debug) {
          if (isExistsPath(tmpfilename)) {
            deleteFile(tmpfilename);
          }
        }

        vscode.window.setStatusBarMessage('$(markdown) ' + exportFilename, StatusbarMessageTimeout);
      } catch (error) {
        showErrorMessage('exportPdf()', error);
      }
    } // async
  ); // vscode.window.withProgress
}


function isExistsPath(path) {
  if (path.length === 0) {
    return false;
  }
  try {
    fs.accessSync(path);
    return true;
  } catch (error) {
    console.warn(error.message);
    return false;
  }
}

function isExistsDir(dirname) {
  if (dirname.length === 0) {
    return false;
  }
  try {
    if (fs.statSync(dirname).isDirectory()) {
      return true;
    } else {
      console.warn('Directory does not exist!') ;
      return false;
    }
  } catch (error) {
    console.warn(error.message);
    return false;
  }
}

function deleteFile (path) {
  var rimraf = require('rimraf')
  rimraf.sync(path);
}

function getOutputDir(filename, resource) {
  try {
    var outputDir;
    if (resource === undefined) {
      return filename;
    }
    var outputDirectory = vscode.workspace.getConfiguration('markdown-pdf')['outputDirectory'] || '';
    if (outputDirectory.length === 0) {
      return filename;
    }

    // Use a home directory relative path If it starts with ~.
    if (outputDirectory.indexOf('~') === 0) {
      outputDir = outputDirectory.replace(/^~/, os.homedir());
      mkdir(outputDir);
      return path.join(outputDir, path.basename(filename));
    }

    // Use path if it is absolute
    if (path.isAbsolute(outputDirectory)) {
      if (!isExistsDir(outputDirectory)) {
        showErrorMessage(`The output directory specified by the markdown-pdf.outputDirectory option does not exist.\
          Check the markdown-pdf.outputDirectory option. ` + outputDirectory);
        return;
      }
      return path.join(outputDirectory, path.basename(filename));
    }

    // Use a workspace relative path if there is a workspace and markdown-pdf.outputDirectoryRootPath = workspace
    var outputDirectoryRelativePathFile = vscode.workspace.getConfiguration('markdown-pdf')['outputDirectoryRelativePathFile'];
    let root = vscode.workspace.getWorkspaceFolder(resource);
    if (outputDirectoryRelativePathFile === false && root) {
      outputDir = path.join(root.uri.fsPath, outputDirectory);
      mkdir(outputDir);
      return path.join(outputDir, path.basename(filename));
    }

    // Otherwise look relative to the markdown file
    outputDir = path.join(path.dirname(resource.fsPath), outputDirectory);
    mkdir(outputDir);
    return path.join(outputDir, path.basename(filename));
  } catch (error) {
    showErrorMessage('getOutputDir()', error);
  }
}

function mkdir(path) {
  if (isExistsDir(path)) {
    return;
  }
  var mkdirp = require('mkdirp');
  return mkdirp.sync(path);
}

function readFile(filename, encode) {
  if (filename.length === 0) {
    return '';
  }
  if (!encode && encode !== null) {
    encode = 'utf-8';
  }
  if (filename.indexOf('file://') === 0) {
    if (process.platform === 'win32') {
      filename = filename.replace(/^file:\/\/\//, '')
    } else {
      filename = filename.replace(/^file:\/\//, '');
    }
  }
  if (isExistsPath(filename)) {
    return fs.readFileSync(filename, encode);
  } else {
    return '';
  }
}

function convertImgPath(src, filename) {
  try {
    var href = decodeURIComponent(src);
    href = href.replace(/("|')/g, '')
          .replace(/\\/g, '/')
          .replace(/#/g, '%23');
    var protocol = url.parse(href).protocol;
    if (protocol === 'file:' && href.indexOf('file:///') !==0) {
      return href.replace(/^file:\/\//, 'file:///');
    } else if (protocol === 'file:') {
      return href;
    } else if (!protocol || path.isAbsolute(href)) {
      href = path.resolve(path.dirname(filename), href).replace(/\\/g, '/')
                                                      .replace(/#/g, '%23');
      if (href.indexOf('//') === 0) {
        return 'file:' + href;
      } else if (href.indexOf('/') === 0) {
        return 'file://' + href;
      } else {
        return 'file:///' + href;
      }
    } else {
      return src;
    }
  } catch (error) {
    showErrorMessage('convertImgPath()', error);
  }
}



// series of makes.


function makeFrame(filename) {
  try {
    let frame = readFile(filename);
    if (frame) {
      return '\n' + frame + '\n';
    } else {
      return '';
    }
  } catch (error) {
    showErrorMessage("makeFrame()",error);
  }
}


function makeStyle(filename) {
  try {
    let css = readFile(filename);
    if (css) {
      return '\n<style>\n' + css + '\n</style>\n';
    } else {
      return '';
    }
  } catch (error) {
    showErrorMessage('makeStyle()', error);
  }
}


function makeScript(filename) {
  try {
    let script = readFile(filename);
    if (script) {
      return '\n<script>\n' + script + '\n</script>\n';
    } else {
      return '';
    }
  } catch (error) {
    showErrorMessage("makeScript()",error);
  }
}



// series of reads.


function readFrames(uri) {
  try {
    let frame='';
    let filePath='';
    let fileName='';
    let filePlace='';
    let frameList=[];
    let i=0;

    // 0. read the frame of template.
    filePath = path.join(myGroundPath,'template','template.html');
    frame += makeFrame(filePath);
    vscode.window.showInformationMessage("get template.html");

    // 1. read the frame of plugins.
    filePlace = path.join(myGroundPath,'plugins')
    frameList = myPluginList;
    if (frameList && Array.isArray(frameList) && frameList.length > 0) {
      for (i = 0; i < frameList.length; i++) {
        fileName = frameList[i] + ".html";
        filePath = path.join(filePlace,frameList[i],fileName);
        frame += makeFrame(filePath);
        }
        vscode.window.showInformationMessage("get frame plugins");
      }
    
    vscode.window.showInformationMessage("finish readFrames!");
    return frame;
  } catch (error) {
    showErrorMessage('readFrames()', error);
  }
}


function readStyles(uri) {
  try {
    let style='';
    let filePath='';
    let fileName='';
    let filePlace='';
    let styleList=[];
    let i=0;
    let href='';

    var includeDefaultStyles = vscode.workspace.getConfiguration('markdown-pdf')['includeDefaultStyles'];
    var highlightStyle = vscode.workspace.getConfiguration('markdown-pdf')['highlightStyle'] || '';
    var ishighlight = vscode.workspace.getConfiguration('markdown-pdf')['highlight'];

    // 0. read the frame of template.
    filePath = path.join(myGroundPath,'template','template.css');
    style += makeStyle(filePath);
    vscode.window.showInformationMessage("get template.css");

    // 1. read the style of the vscode.
    if (includeDefaultStyles) {
      filePath = path.join(__dirname, 'styles', 'markdown.css');
      style += makeStyle(filePath);
    }

    // 2. read the style of the markdown.styles setting.
    if (includeDefaultStyles) {
      styleList = vscode.workspace.getConfiguration('markdown')['styles'];
      if (styleList && Array.isArray(styleList) && styleList.length > 0) {
        for (i = 0; i < styleList.length; i++) {
          href = fixHref(uri, styleList[i]);
          style += '<link rel=\"stylesheet\" href=\"' + href + '\" type=\"text/css\">';
        }
      }
    }

    // 3. read the style of the highlight.js.
    if (ishighlight) {
      if (highlightStyle) {
        var css = vscode.workspace.getConfiguration('markdown-pdf')['highlightStyle'] || 'github.css';
        filePath = path.join(__dirname, 'node_modules', 'highlight.js', 'styles', css);
        style += makeStyle(filePath);
      } else {
        filePath = path.join(__dirname, 'styles', 'tomorrow.css');
        style += makeStyle(filePath);
      }
    }

    // 4. read the style of the markdown-pdf.
    if (includeDefaultStyles) {
      filePath = path.join(__dirname, 'styles', 'markdown-pdf.css');
      style += makeStyle(filePath);
    }

    // 5. read the style of the markdown-pdf.styles settings.
    styleList = vscode.workspace.getConfiguration('markdown-pdf')['styles'] || '';
    if (styleList && Array.isArray(styleList) && styleList.length > 0) {
      for (i = 0; i < styleList.length; i++) {
        href = fixHref(uri, styleList[i]);
        style += '<link rel=\"stylesheet\" href=\"' + href + '\" type=\"text/css\">';
      }
    }
    // 6. read the frame of plugins.
    filePlace = path.join(myGroundPath,'plugins')
    styleList = myPluginList;
    if (styleList && Array.isArray(styleList) && styleList.length > 0) {
      for (i = 0; i < styleList.length; i++) {
        fileName = styleList[i] + ".css";
        filePath = path.join(filePlace,styleList[i],fileName);
        style += makeStyle(filePath);
        }
        vscode.window.showInformationMessage("get style plugins");
      }
    
    vscode.window.showInformationMessage("finish readFrames!");
    return style;
  } catch (error) {
    showErrorMessage('readStyles()', error);
  }
}


function readScripts(uri) {
  try {
    let script='';
    let filePath='';
    let fileName='';
    let filePlace='';
    let scriptList=[];
    let i=0;

    // 0. read the frame of template.
    filePath = path.join(myGroundPath,'template','template.js');
    script += makeScript(filePath);
    vscode.window.showInformationMessage("get template.js");

    // 1. read the frame of plugins.
    filePlace = path.join(myGroundPath,'plugins')
    scriptList = myPluginList;
    if (scriptList && Array.isArray(scriptList) && scriptList.length > 0) {
      for (i = 0; i < scriptList.length; i++) {
        fileName = scriptList[i] + ".js";
        filePath = path.join(filePlace,scriptList[i],fileName);
        script += makeScript(filePath);
        }
        vscode.window.showInformationMessage("get script plugins");
      }

    vscode.window.showInformationMessage("finish readScripts!");
    return script;
  } catch (error) {
    showErrorMessage('readScripts()', error);
  }
}



/*
 * vscode/extensions/markdown-language-features/src/features/previewContentProvider.ts fixHref()
 * https://github.com/Microsoft/vscode/blob/0c47c04e85bc604288a288422f0a7db69302a323/extensions/markdown-language-features/src/features/previewContentProvider.ts#L95
 *
 * Extension Authoring: Adopting Multi Root Workspace APIs ?E Microsoft/vscode Wiki
 * https://github.com/Microsoft/vscode/wiki/Extension-Authoring:-Adopting-Multi-Root-Workspace-APIs
 */
function fixHref(resource, href) {
  try {
    if (!href) {
      return href;
    }

    // Use href if it is already an URL
    const hrefUri = vscode.Uri.parse(href);
    if (['http', 'https'].indexOf(hrefUri.scheme) >= 0) {
      return hrefUri.toString();
    }

    // Use a home directory relative path If it starts with ^.
    if (href.indexOf('~') === 0) {
      return vscode.Uri.file(href.replace(/^~/, os.homedir())).toString();
    }

    // Use href as file URI if it is absolute
    if (path.isAbsolute(href)) {
      return vscode.Uri.file(href).toString();
    }

    // Use a workspace relative path if there is a workspace and markdown-pdf.stylesRelativePathFile is false
    var stylesRelativePathFile = vscode.workspace.getConfiguration('markdown-pdf')['stylesRelativePathFile'];
    let root = vscode.workspace.getWorkspaceFolder(resource);
    if (stylesRelativePathFile === false && root) {
      return vscode.Uri.file(path.join(root.uri.fsPath, href)).toString();
    }

    // Otherwise look relative to the markdown file
    return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href)).toString();
  } catch (error) {
    showErrorMessage('fixHref()', error);
  }
}

function checkPuppeteerBinary() {
  try {
    // settings.json
    var executablePath = vscode.workspace.getConfiguration('markdown-pdf')['executablePath'] || ''
    if (isExistsPath(executablePath)) {
      INSTALL_CHECK = true;
      return true;
    }

    // bundled Chromium
    const puppeteer = require('puppeteer-core');
    executablePath = puppeteer.executablePath();
    if (isExistsPath(executablePath)) {
      return true;
    } else {
      return false;
    }
  } catch (error) {
    showErrorMessage('checkPuppeteerBinary()', error);
  }
}



/*
 * puppeteer install.js
 * https://github.com/GoogleChrome/puppeteer/blob/master/install.js
 */
function installChromium() {
  try {
    vscode.window.showInformationMessage('[Markdown PDF] Installing Chromium ...');
    var statusbarmessage = vscode.window.setStatusBarMessage('$(markdown) Installing Chromium ...');

    // proxy setting
    setProxy();

    var StatusbarMessageTimeout = vscode.workspace.getConfiguration('markdown-pdf')['StatusbarMessageTimeout'];
    const puppeteer = require('puppeteer-core');
    const browserFetcher = puppeteer.createBrowserFetcher();
    const revision = require(path.join(__dirname, 'node_modules', 'puppeteer-core', 'package.json')).puppeteer.chromium_revision;
    const revisionInfo = browserFetcher.revisionInfo(revision);

    // download Chromium
    browserFetcher.download(revisionInfo.revision, onProgress)
      .then(() => browserFetcher.localRevisions())
      .then(onSuccess)
      .catch(onError);

    function onSuccess(localRevisions) {
      console.log('Chromium downloaded to ' + revisionInfo.folderPath);
      localRevisions = localRevisions.filter(revision => revision !== revisionInfo.revision);
      // Remove previous chromium revisions.
      const cleanupOldVersions = localRevisions.map(revision => browserFetcher.remove(revision));

      if (checkPuppeteerBinary()) {
        INSTALL_CHECK = true;
        statusbarmessage.dispose();
        vscode.window.setStatusBarMessage('$(markdown) Chromium installation succeeded!', StatusbarMessageTimeout);
        vscode.window.showInformationMessage('[Markdown PDF] Chromium installation succeeded.');
        return Promise.all(cleanupOldVersions);
      }
    }

    function onError(error) {
      statusbarmessage.dispose();
      vscode.window.setStatusBarMessage('$(markdown) ERROR: Failed to download Chromium!', StatusbarMessageTimeout);
      showErrorMessage('Failed to download Chromium! \
        If you are behind a proxy, set the http.proxy option to settings.json and restart Visual Studio Code. \
        See https://github.com/yzane/vscode-markdown-pdf#install', error);
    }

    function onProgress(downloadedBytes, totalBytes) {
      var progress = parseInt(downloadedBytes / totalBytes * 100);
      vscode.window.setStatusBarMessage('$(markdown) Installing Chromium ' + progress + '%' , StatusbarMessageTimeout);
    }
  } catch (error) {
    showErrorMessage('installChromium()', error);
  }
}

function showErrorMessage(msg, error) {
  vscode.window.showErrorMessage('ERROR: ' + msg);
  console.log('ERROR: ' + msg);
  if (error) {
    vscode.window.showErrorMessage(error.toString());
    console.log(error);
  }
}

function setProxy() {
  var https_proxy = vscode.workspace.getConfiguration('http')['proxy'] || '';
  if (https_proxy) {
    process.env.HTTPS_PROXY = https_proxy;
    process.env.HTTP_PROXY = https_proxy;
  }
}

function setBooleanValue(a, b) {
  if (a === false) {
    return false
  } else {
    return a || b
  }
}

function init() {
  try {
    if (checkPuppeteerBinary()) {
      INSTALL_CHECK = true;
    } else {
      installChromium();
    }
  } catch (error) {
    showErrorMessage('init()', error);
  }
}

感想

readCssからmakeStyleが出てくるの違和感がありすぎるのでreadStyleにしてやった,それらと同様の流れでframe(htmlファイル)とscript(jsファイル)を扱えるようにした,あと変更する範囲でvarの使用をなるべくやめさせた,変数宣言で型を明示したくなった,なぜかvscode上でコンソールが表示されなかったがvscode.window.showErrorMessageは役立った,ブラウザでコンソール見るのは大事だと思った

vscode上でmarkdown pdf + katexで速攻で数式をpdf化

準備

とりあえずこれを入れよう

最近のvscodeはデフォルトでmath形式びmdファイルもちゃんと表示してくれるらしい

概要

vscode上でmarkdownをプレビューする時に数式を扱えるmarkdown mathやvscode内蔵のビュワーは,内部でkatexを動かしてレンダリングしながら表示しているらしく,またそのkatexはブラウザやサーバーサイドにおいてhtml+cssで完結して動くため,svgを弄ってmathmlを設定するmathjaxよりも更に軽量なアドオンらしく,そしてそのmarkdown pdfはmarkdownからhtml+cssを経由したのちにpdfに変換している…
つまりhtml+cssレンダリング処理に介入して(オプションを使って)katexを動作させれば,markdownを爆速でpdfへと変換できるね(デフォルトでは何も設定されていないため数式はレンダリングされないよ),という流れ

手順

  • コマンドパレットから拡張機能フォルダを開いてお目当てのtemplate.htmlを探す
    • macならば/Users/[ユーザー名]/.vscode/extensions/yzane.markdown-pdf-1.4.4/template/template.htmlに確実にあるはず,Windows?知らん
  • katex.org
    • をよく読む
  • katex.org
    • をよく読む
  • katex.org
    • をよく読む
  • そして以下をコピペ
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/katex.min.css" integrity="sha384-R4558gYOUz8mP9YWpZJjofhk+zx0AS11p36HnD2ZKj/6JR5z27gSSULCNHIRReVs" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/katex.min.js" integrity="sha384-z1fJDqw8ZApjGO3/unPWUPsIymfsJmyrDVWC8Tv/a1HeOtGmkwNd/7xUS0Xcnvsx" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/contrib/auto-render.min.js" integrity="sha384-+XBljXPPiv+OzfbB3cVmLHf4hdUFHlWNZN5spNQ7rmHTXpd7WvJum6fIACpNNfIR" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/contrib/mathtex-script-type.min.js" integrity="sha384-jiBVvJ8NGGj5n7kJaiWwWp9AjC+Yh8rhZY3GtAX8yU28azcLgoRo4oukO87g7zDT" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/contrib/mhchem.min.js" integrity="sha384-UEY9IRPkV+TTTY7nK1wSrfhWPDJy9wr4PmYg3DLPcN5F4NDlIwGZkWtWveKR/45c"  crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/contrib/copy-tex.min.css" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/katex@0.15.1/dist/contrib/copy-tex.min.js" integrity="sha384-Ep9Es0VCjVn9dFeaN2uQxgGcGmG+pfZ4eBaHxUpxXDORrrVACZVOpywyzvFRGbmv" crossorigin="anonymous"></script>
<script>
    document.addEventListener("DOMContentLoaded", function() {
        renderMathInElement(document.body, {
          delimiters: [
              {left: '$$', right: '$$', display: true},
              {left: '$', right: '$', display: false}
          ],
          throwOnError : false,
          output:"mathml",
          strict: false,
        });
    });
</script>
  • これらを{{{style}}}{{{mermaid}}}の間に(<head><\head>の中に)挟んでおく
  • {{{style}}}{{{mermaid}}}{{{content}}}レンダリング結果を注入するのに必要な起点なので消すと動かなくなる
  • delimiterは好みに書き換えても良いが困らなければこのままで良い(このままの方が良い)
  • 恐らくkatexのheadバージョンが変われば先のページも書き変わるので(srcに載ってるURLにもSSHのオプション引数が設定されているし),気が向いた際にちゃんと確認しておこう

注意点

ブラウザにレンダリングをさせるための工夫

throwOnError : falseoutput:"mathml"が必要,エラーはなるべくは無視してくれる方が良いし,"html"では後述のcopy-tex extensionが動作しなくなる,現状では"mathml"で困ることもなさそうなので

ディスプレイモードで改行を行うための工夫

strict: falseが必要かつ改行には\\\\が必要,前者はkatexの改行制限を外すため,後者は中間生成の時点で働く謎のエスケープ処理を回避するため
もし挙動が変だと思った時は一旦htmlで出力してデバッグするべし,markdown pdfの処理が自分の想定外である場合が大半

アンダースコア_の扱いの工夫

markdown pdfが強調タグ(<em><\em>)に変換しかねない,発生条件は定かではないが|_{}すると50%の確率で<em><\em>のペアに化かしてくる,必ずエスケープした\|\_{}を使う癖をつけるべし

字体の変化の正当な手順

現在のlatexでは\rm{}が正当なやり方とされているが,katexでは{\rm }を使わないと変な処理が起きて困る,理由は不明だが古い処理系を使っているせいと思われる

その他の環境を使いたい

ディスプレイモードに限定されるが,$$で囲った中身でbegin{xxx}end{xxx}を使う必要がある,そこを直で呼び出せるmathjaxとは訳が違うらしい
例)

$$
  \begin{align}
    x &= y \\\\
    &= z
  \end{align}
$$

もちろん改行には\\\\を使う

拡張機能

恐らくこれで全部のはず

mathJax後方互換

mathtex-script-type.min.jsを使う
mathタグで有効化することでmathjaxをまんま置き換えられるらしい

化学記法互換

mhchem.min.jsを使う
化学に関する記法を追加するらしい

mathmlコピペ互換

copy-tex.min.csscopy-tex.min.jsを使う
mdからhtmlに出力した際にクリップボードからlatex記法のコードをそのまま取り出せるようになる,pdfには対応していないが(ブラウザ上のclipboard apiに相当する機能がないので)htmlで見れる環境なら絶対に採用するべし

vscodeの設定用json + markdown pdf

場所

既定の(デフォルト)設定

所在不明,呼び出しても読み取り専用エディタとして出てくるだけ,どこに実体があるのかはさっぱり…

ユーザー(グローバル)設定

/Users/[ユーザ名]/Library/Application Support/Code/User/settings.json

普段から使い回したい設定は全てこちらに書いておく,latexmk用の起爆ソースとか
デフォルト設定をオーバーライドできる,対象はほぼ全てのオプション

ワークスペース(ウィンドウ)設定

workspace.code-workspace

ワークスペースで限定的に使いたい時に書く,エディタのテーマを切り替えるとか
グローバル設定をオーバーライドできる,対象はほぼ全てのオプション

vscodeワークスペースとは即ちウィンドウ1つのこと,処理のカスタマイズはなるべく書かない方が良い気がする

フォルダー(ローカル)設定

.vscode/settings.json

処理のカスタマイズなど環境を汚染したくない時に書く,今回はココを弄ってみる
ディレクトリに1つだけ作成できて,ワークスペースを含めてそのフォルダを開いたら必ず共有される
なおworkspace.code-workspaceが存在しない場合はなぜかこれがワークスペース設定としてGUIに表示されるし,設定範囲はワークスペース設定と同じになっている,意味不明というか初見のユーザーに優しくないね…

ローカル設定をオーバーライドできる,対象は一部のオプション(ここでしか変えられないものもあるみたいだが…)

使い所

試しにmarkdown pdfの書式を弄ってみる

以下コピペ

書式の一例,.vscode/settings.jsonに以下をコピペして成形すると良い

{
    "markdown-pdf.headerTemplate":
        "<div style=\"font-size: 12px; margin:0 12px 0 12px; width:60%;\"><span style=\"float: left;\">ここにタイトル</span></div> <div style=\"font-size: 12px; margin:0 12px 0 12px; width:30%;\"><span style=\"float: left;\">ここに名前</span></div> <div style=\"font-size: 12px; margin:0 12px 0 12px; width:10%;\"><span style=\"float: right;\" class='date'></span></div>",
    "markdown-pdf.footerTemplate":
        "<div style=\"font-size: 12px; margin:0 12px 0 12px; width:60%;\"><span style=\"float: right;\">ここに署名</span></div> <div style=\"font-size: 9px; margin:0 12px 0 12px; width:40%;\" ><span class='pageNumber'></span> ページ目 / <span class='totalPages'></span> ページ中</div>"
}

どうやら環境変数や引数をブチ込むみたいな芸当はこれだけでは無理らしい(できるの?)

バグ?

<span class='pageNumber'></span><span class='totalPages'></span>は現在ページと合計ページをそれぞれ出力する,しかし内外からその文字サイズを9pxより大きく設定してしまうとなぜか表示されなくなって詰む
また最外の<div>に上下向きのmarginを設定するとレイアウトが崩れてフッタが本文に隠されてしまい詰む,それどころかpaddingしても詰むし文字が大きすぎても詰むので,あまり大きく弄らない方が吉

前者はmarkdown pdfがやりたいhtml+cssにおける動的な注入に対して不都合であり,どうやらmarkdown-pdfのさらに実装元のプラグインであるpuppeteerにまで問題が遡るらしい,"puppeteer"+"pageNumber"+"font size"で検索してみれば「10pxにすると出ますよ」「9pxにすると出ますよ」みたいな内容が見つかる

後者はmarkdown-pdf > margin:bottommarkdown-pdf > margin:topの値を弄れば解決するが,あんまり大きくても見栄えが悪いし何ならタイトルページは別に作れば良いので,なるべくはスマートになるよう心がけよう

LaTeXとの比較

Markdownなので機能が軽いとは言えど学習コストも抑えられて設定も楽でコンパイルも手間や時間がかからずhtmlもodfも生成できると考えるとクッソ良い(個人の感想)

サンプル

htmlの時点でこんな感じ
見出しは6段階までらしい

example
example

# welcome

## to

### my

#### slightly

##### beautiful

###### dream

####### ...?


OK.

````zsh
sudo rm -rf /
````

$$
  e^{i \pi} + 1 = 0
$$

$$
\begin{pmatrix}
  1 & 0 \\\\
  0 & 1
\end{pmatrix} \\\\
against, \\\\
\begin{vmatrix}
  1 & 0 \\\\
  0 & 1
\end{vmatrix}
$$

この世をば 我が世とぞ思ふ 望月の

欠けたることも 無しと思へば
body {
    color: white;
    background-color: black
}
{
    "markdown-pdf.headerTemplate":
        "<div style=\"font-size: 12px; margin:0 12px 0 12px; width:60%;\"><span style=\"float: left;\">this is my report</span></div> <div style=\"font-size: 12px; margin:0 12px 0 12px; width:30%;\"><span style=\"float: left;\">Soluna Eureka</span></div> <div style=\"font-size: 12px; margin:0 12px 0 12px; width:10%;\"><span style=\"float: right;\" class='date'></span></div>",
    "markdown-pdf.footerTemplate":
        "<div style=\"font-size: 12px; margin:0 12px 0 12px; width:60%;\"><span style=\"float: right;\">tuned by S.E.</span></div> <div style=\"font-size: 9px; margin:0 12px 0 12px; width:40%;\" ><span class='pageNumber'></span> page of <span class='totalPages'></span> pages </div>",
    "markdown-pdf.margin.top": "2cm",
    "markdown-pdf.margin.bottom": "2cm",
    "markdown-pdf.highlightStyle": "night-owl.css",
    "markdown-pdf.styles": [
        "./change.css"
    ],
    "markdown-pdf.type": [
        "pdf",
        "html"
    ],
    "markdown-pdf.orientation": "landscape",
    "markdown-pdf.convertOnSave": true,
}

なんと無理矢理に横長にできてしまうし背景色もコードブロックのスタイルも弄れるのだ

だがpdfにすると余白領域が真っ白のまま,ダサ過ぎないか…?

example2
example2

コアのカスタマイズ

タイムアウトの阻止

いくらmdファイルが長くともhtmlレンダリングは一瞬で終わるが,pdfレンダリングは処理時間が30000ms(30秒)をオーバーして強制停止されることでお目当てのブツがエクスポートされないケースがある.だいたいpuppeteerの仕様のせいだが元々は異常動作をさせないためにかけられた防御であって,これをクリアするには手動でtimeout時間を0に設定する必要がある(その代わりセキュリティリスクは高まるらしいが).

github.com

具体的には/Users/[ユーザ名]/.vscode/extensions/yzane.markdown-pdf-[バージョン表記]/extension.jsの407行目あたりで

await page.goto(vscode.Uri.file(tmpfilename).toString(), { waitUntil: 'networkidle0', timeout:0});

要はawaitの呼び出し先の関数page.gotoの引数にtimeout:0を入れてやれば良い.
行数が違くても下の行に

// generate pdf
// https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagepdfoptions

とか書いてあるしわかりやすいと思われる.つーか多分このことを言ってるんじゃないかな…?

github.com

せや!結局は`streamlink`で`ffmpeg`を使えばええ!

準備

ffmpegstreamlinkが依存してる)とstreamlinkbrewで入れる

formulae.brew.sh

formulae.brew.sh

brew install ffmpeg

brew install streamlink

以下の通り環境変数PATHffmpegがないと死なので確認する

man streamlink
...
 FFmpeg options

       --ffmpeg-ffmpeg FILENAME
              FFMPEG is used to access or mux separate video and audio streams. You can specify the location of the
              ffmpeg executable if it is not in your PATH.

              Example: "/usr/local/bin/ffmpeg"
...

使い方

hlsでも非hlsでも大丈夫

  1. 対象のwebサービスに行って動画を再生して
  2. 「webインスベクタ」を表示,「ネットワーク」を表示
  3. 頑張って拡張子が.m3u8.mp4のファイルを検索
  4. 前者がhls,後者が非hls
  5. そのファイルを落とした時のcURLをコピー
  6. cURLから標準urlだけを抽出
  7. streamlink '[url]' best -o temp
  8. ffmpeg -i tmp -c copy file.mp4

何が起きてるか

streaming系サービスでのhttp通信をうまいこと管理して,hls処理ができるffmpegに渡してくれてる
ffmpegに不足してるhttp管理機能とcurlに不足してるhls処理機能を両立してくれてこれは…ありがたい

細かいオプション

ffmpeg.org

streamlink.github.io

ニコ動のオプション機能でログイン可能とかセッションID持ち込みとかあったりするらしいな

利点!

独立してセッションを構築してくれるからクッソ楽
もしそれができない事情があるなら適宜にhttpのヘッダ情報を引数で渡せば良いので問題はない

欠点?

処理をせず.m3u8+.tsの形式に落とすのは想定外の動作
hlsだと.mp4になったりならなかったりするのでもっかいffmpegをかける習慣をつけよう
まぁ変換前の形式で落とす必要性もなさそうだし,なんならhlsへの変換もやれるっぽいし(今回はそこには触れないけど)

現行のmacOSで使えるデータの圧縮・暗号

圧縮と暗号は大事

匿名化に加えてこれら3つは3大PCリテラシーだと思います

パスワード付きPDF

ちょっとした書類ならコレで良いと思う

  • プレビューで何らかをpdfを保存するときに
  • 「アクセス権」→「書類を開くときにパスワードを要求」にチェック
  • 専用パスワードを確実に打ちこむ
  • 「アクセス権」の設定でさらに改竄防止を見込める
    • 例外的な許可を出さずにパスワード設定すればおk

PDFの仕様なんてそうそう変わらんし,深くは追求しません,汎用性も低いでしょうし…

パスワード付きで暗号と圧縮

ちょっとしたデータならコレで良いと思う
基本的にFInderからGUI経由で行うことが無理な(オプションが呼び出せない)上に,macOSにデフォルトで入ってるアプリやコマンドは更新性や追従性に乏しく,これはhomebrew経由で外部のツールを導入した方が良いと思う

準備

とりあえずは基本のziprartarを抑えておきたい

formulae.brew.sh

formulae.brew.sh

formulae.brew.sh

えっunrarってつい最近ライセンス問題で公式から消えたらしいですよ

qiita.com

formulae.brew.shuntar は存在しない,動作は全部コマンドで決め打ちしているらしい

  • 通常のbrew環境が構築されてるintel macが前提
    • apple siliconでのhomebrewは色々とパスが違ってくるらしいけど知らん
      • とりあえずRosetta2でやればいいんじゃないのか?
  • コマンド設定
    • 以下の順番を守って取得
      1. brew install zip
      2. brew install unzip
      3. brew install rar
      4. brew install carlocab/personal/unrar
      5. brew install gnu-tar
  • パスを設定
    • rarunrarはデフォルトで/usr/local/binエイリアスが発生する
    • targtarとして/usr/local/binエイリアスがある
    • zipunzipmacデフォルトと衝突すると厄介なことになるらしくエイリアスが生成されていない
      • gzipgzipとして別にあるせいで名前が被ってるせいかもしれんね
    • なので/usr/local/opt/binを作ってパスに通した上で/usr/local/opt/[cask name]/bin/[cask command]シンボリックリンクを置く
      • ついでに同じようにtarも置いとこう
      • mkdir /usr/local/opt/bin
      • ln -s /usr/local/opt/zip/bin/zip /usr/local/opt/bin/zip
      • ln -s /usr/local/opt/unzip/bin/unzip /usr/local/opt/bin/unzip
      • ln -s /usr/local/opt/gnu-tar/bin/gtar /usr/local/opt/bin/tar
      • echo export PATH='/usr/local/opt/bin:$PATH' >> ~/.bashrc
    • 私はPATH = [application path]/usr/local/bin:/usr/local/opt/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbinみたいにしてる
    • 各自のシェルの設定ファイルに合わせること,zshなら~/zprofile~/.zshrcになる
      • bashzshの間なら複製して名前を変えるだけでも割と行けるっぽい…?

実行

zipの場合

  • 暗号
    • zip -A -o -r -n .zip:.bz2:.7z:.rar:.gz:.tgz -X -y -8 -e [zipped file path] [source file path]
      • 例:zip -A -o -r -n .zip:.bz2:.7z:.rar:.gz:.tgz -X -y -8 -e ~/Documents/Private/data.zip ~/Download/list
    • パスワード設定はダブルチェックされる
    • オプションはそれぞれ,
      • -AWindows用の自己解凍.exeファイルが生成
        • つまりmacOSには関係ねぇ
      • -oで出てくる.zipの日付がディレクトリ内の日付の最新版に
      • -r再帰処理にしてディレクトリに対応
      • -n [.extension 1]:[.extension num]で2重圧縮を回避
      • -Xファイルシステムの余計な情報を削除
      • -yで参照先の外部ファイルの混入を阻止
      • -8で圧縮強度を最大化
      • -eで本体を暗号化
    • [zipped file path]のファイル名はそのまま出てくるので末尾に.zipを忘れないように
  • 分割
    • zip -s [num][kmgt] [source file path] -O [zipped file path]
      • 例:zip -s 64k ~/Documents/Private/data.zip -O ~/Documents/Private/spirit.zip
    • 1発目の.zip生成の段階で分割しようとするとエラーを吐くのでNG
    • [source file path]ファイル名は.zipじゃないと認識してくれないので注意
    • パスワードチェックなし
    • オプションはそれぞれ,
      • -s [num][k,m,g,t]で分割サイズを指定,最低でも64kbyte以上
  • 結合
    • zip -s 0 [source file path] -O [zipped file path]
      • 例:zip -s 0 ~/Documents/Private/spirit.zip -O ~/Documents/Private/data2.zip
    • -s 0で1つの.zipに還元される
      • 分割と結合を繰り返すとデータがバグるかもしれん
    • パスワードチェックなし
    • [source file path]には分割時の代表ファイル(.z[num]じゃないもの)を指定する
      • もちろん例に漏れず.zipじゃないと認識してくれない
  • 復号
    • unzip [source file path]
      • 例:unzip ~/Documents/Private/data2.zip
      • 特定のファイル1つを解凍する場合はunzip -p [source file path] > [unzipped file path]で名前指定も可能
        • -pで出力をstdoutに指定してパイプ化しただけ
      • 別のディレクトリに入れる場合はunzip [source file path] -d [any directory path]で押し込める
        • -dとその引数は必ず最後に置くこと
    • パスワードチェック必須
    • .zipじゃなくても読み込んでくれる(中身がダメならダメと言ってくれる)
    • 名前がダブったファイルの強制上書き許可は-o・強制上書き禁止は-n
      • unzip -B [source file path]すると上書きはせずにバージョン別(拡張子を含むファイル名の末尾に~[num])のバックアップが追加される
        • 解凍ファイルの内部の処理ではなく,解凍された中身が生成される該当のディレクトリに
      • これ自体にバージョン管理機能はなく,やりたいなら番号をワイルドカードで全検索して抽出しろって感じか?
      • -oするとそのバックアップすらも上書きできるので注意
    • 先の通りmacOSでは自己解凍はできない

ここでは単純に圧縮と展開のやり方を示したが,本来はアーカイブ更新機能の側面が強く,元ディレクトリとのファイルの差分をとって更新して保存するやり方が一般的かもしれない.基本的に.zipにはロックをかけずアップデートをかけ続ける,それを自動化して低リソースな環境で実行する…みたいな雰囲気を感じる.
そこらが関係するのか軽い気持ちで調べてたら想像以上に複雑だったし,私が望む「使える・壊れない・明かされない」という観点でもzipよりrarの方が良かったりする,そのためアーカイブ更新機能として使いこなすのはまた別の機会にしたい.

rarの場合

  • 暗号
    • rar a -k -r -ts- -s -ds -sfx -ma5 -m5 -hp [rarred file path] [source file path]
    • rar a -k -r -ts- -s -ds -sfx -ma5 -m5 -hp ~/Documents/Private/data.sfx ~/Download/list
    • パスワード設定はダブルチェックされる
    • オプションはそれぞれ,
      • aアーカイブ生成モードを指定
      • -kでロックして再編集を不可能に
      • -rで再起処理にしてディレクトリに対応
      • -ts-で時間系の情報を削除
      • -sでソリッド圧縮を指定
      • -dsで内部の名前順ソートを無効化
      • -sfxで自己解凍実行ファイルを添付
      • -ma5で新しい.rar規格を使用
      • -m5で圧縮強度を最大化
      • -hpでヘッダごと本体を暗号化
    • -sfxの場合は拡張子.sfxが必ずつくが,ない場合は[rarred file path]がそのまま出てくる
  • 復号
    • unrar x [source file path]
      • 自己解凍させたい場合は./[source file path]すると起動する,手順は変わらない
      • 特定のファイル1つを解凍する場合はunrar p -iunl [source file path] > [unrarred file path]で名前指定も可能
        • -pで出力をstdoutに指定した上で-inulでデフォの出力を消さないとダメ
      • 別のディレクトリに入れる場合はunrar x [source file path] [any directory path+/]で押し込める
        • パスの最後に/が入ってない場合は[source file path]と見なされて処理されないので注意
    • パスワードチェック必須
    • .rarじゃなくても読み込んでくれる(中身がダメならダメと言ってくれる)
    • 名前がダブったファイルの強制上書き許可は-o+・強制上書き禁止は-o-

ロックと暗号化が行えるのが強みで,圧縮性・耐破性・暗号性もzipより上,導入も楽で自爆解凍もできる,しかも文法が簡単で覚えやすい.現実では特に欧州で流行ってるらしく,実際に使っている人間は自分の周りでこそほぼ見たことがないものの,割と積極的に使いたい印象を受けた.もうこれだけでよくね?と言える程度には簡潔に完結していると思う.

zipに関する補記

homebrewで入ってくるzipは「16 June 2008 (v3.0)」かつunzipは「unzip20 April 2009 (v6.0)」が最新版となっている.macOSにデフォルトで入っているzipunzipも全く同じであり,要するにわざわざディレクトリを分けて管理しようとしたのは徒労であったということになる.さらに-Zbzipできるがunzipできない(以下の通りなら.zip規格の対応が0.1だけunzipの方が遅れているせいだと思われる)などのドボン選択肢もある.

% zip -rZb arc.zip test
% unzip arc.zip
... need PK compat. v4.6 (can do v4.5)

またzipにおける「エントリ」とは「.zip内にあるファイルもしくはディレクトリ」を指す.「エントリには65535個の制限があるほか,アーカイブのサイズにも4GBの制限がある」なる昔話があったらしいが.これはかつてのzipが32bit動作をしているからであり,それを改善したzip64はその制限を超えられるようになった.
自分のzipunzipがそれに対応しているかどうかについて,以下のように表示されれば対応の確認が取れる.

% zip -v | grep 64
... ZIP64_SUPPORT
% zip -v | grep 64
... ZIP64_SUPPORT

現在は/usr/bin/unzipもzip64に対応しているようだが,アーカイブユーティリティ.appについてはmacOS Catalinaになるまで実装されなかったという目撃情報がある.

taiyakon.com

そもそもzipunzipの公式サイトを見ても最新版がhomebrew上の最新版やmacOSのデフォルト版と一致しており,

infozip.sourceforge.net

これらはInfo-zipという形で2009年までOSSで開発されていた,今後ここから新しくなることはないと考えて良さそう.
後継としてはzlibgzipか,前者はzipで培われた圧縮技術を各言語・各OSで利用可能にするライブラリで,後者はGNUの開発元でGPLライセンスで提供されるファイル圧縮用のツール,という認識で合っているのだろうか?

準備

rarzipの代わりにbzip2gzip7ziplrzip(・rzip)を利用していく

formulae.brew.sh

formulae.brew.sh

formulae.brew.sh

formulae.brew.sh

formulae.brew.sh

  • コマンド設定
    • 以下を取得
      1. brew install gzip
      2. brew install p7zip
      3. brew install bzip2
      4. brew install lrzip
      5. brew install rzip
  • パスを設定
    • bzip2macデフォルトと衝突すると厄介なことになるらしくエイリアスが生成されていない
      • ln -s /usr/local/opt/bzip2/bin/bzip2 /usr/local/opt/bin/bzip2しましょうね
    • コレ以外は普通に/usr/local/binに入ってくれる
    • rzipは使わないが,archlinux wikiにも載ってたので敬意を表して数字に貢献した
      • lrziprzipの機能を利用した上でさらに色々と工夫したらしいな?

実行

以下は順に追記していきます

gzipの場合

  • 圧縮
    • gzip -k -9 -c [source file path] > [zipped file path]
    • [zipped file path]のファイル名はそのまま出てくるので末尾に.gzを忘れないように
      • -c> [zipped file path]がないと[source file path]のファイル名に勝手に.gzがつくけど
    • オプションはそれぞれ,
      • -kで元ファイルを削除しない
      • -9で圧縮強度を最大化
      • -cstdoutに指定
    • ディレクトリを再帰的に辿る実装はなく,tar(後述)でまとめてからgzipするのが正攻法
      • 引数に複数のファイルを並べると全て個別に.gz化されるのが特徴
      • gzip -k -9 -c [source file path1] > [zipped file path1] [source file path num] > [zipped file path num]すれば良い
        • [source file path1] [source file path num] > [zipped file path]すると,.tar同士だけなら最後に展開すればなんとかなるが,.tar以外のファイルが混ざってると詰む
        • manのADVANCED USAGEは嘘こそ言ってないが,例えば画像ファイルの2つをgzipしてgunzipすると元に戻らない,フォルダ構造を失った挙句それをrescueする方法がないから当然とも言えるが
    • 名前がダブったファイルの強制上書き許可は-f,なければ聞かれるまでもなく上書きされない
  • 解凍
    • gzip2 -k -c [source file path] > [unzipped file path]
    • オプションはそれぞれ,
      • -kで元ファイルを削除しない
      • -cstdoutに指定
    • どうせファイルしか出てこないのでこれで覚えた方が良い
    • 名前がダブったファイルの強制上書き許可は-f,なければ聞かれるまでもなく上書きされない
    • gzipを入れると勝手にgunzipもついてくる
      • というよりgzipを使う上で-zで圧縮するか-dで解凍するかの違いしかない

調べた限りではディレクトリ統合機能もパスワード暗号化機能もなさそう,やはり別ライブラリ(後述)を使うべきだろう.

--rsyncableとは?

tar+gzipで保管されるファイルに対して,生成する際に--rsyncableを用いてgzipしておくことで,そのサーバに対してrsyncした際に得られるデータの差分の量そのものがカットできる(2016年のv1.7で実装されたらしい)

superuser.com

translate.google.com

Rsyncable gzipbeeznest.wordpress.com

beeznest-wordpress-com.translate.goog

rsync -zオプションでも圧縮が可能であるが,こちらは差分を取った後に転送するデータを圧縮するため,要するに2段構えの方が(マシンリソースあるなら)絶対に良いし,zipに比べても大きな利点となる

gzip -rgunzip -rの動作

この-r再帰的という意味を示すが,zip -rは渡されたパスが示すディレクトリを基準として配下のディレクトリ・ファイルをまとめて1つの.zipにする一方,gzip -rでは渡されたパスが示すディレクトリもしくはその配下のディレクトリにファイルがあればその場で1つずつ.gzにしていく,同様に1つずつ.gzから戻していくのがgunzip -rである

p7zipの場合

bzip2の場合

  • 圧縮
    • bzip2 -k -9 -c [source file path] > [zipped file path]
    • [zipped file path]のファイル名はそのまま出てくるので末尾に.bz2を忘れないように
      • -c> [zipped file path]がないと[source file path]のファイル名に勝手に.bz2がつくけど
    • オプションはそれぞれ,
      • -kで元ファイルを削除しない
      • -9で圧縮強度を最大化
      • -cstdoutに指定
    • ディレクトリを再帰的に辿る実装はなく,tar(後述)でまとめてからbzip2するのが正攻法
      • 引数に複数のファイルを並べると全て個別に.bz2化されるのが特徴
        • bzip2 -k -9 -c [source file path1] > [zipped file path1] [source file path num] > [zipped file path num]すれば良い
        • [source file path1] [source file path num] > [zipped file path]すると,.tar同士だけなら最後に展開すればなんとかなるが,.tar以外のファイルが混ざってると詰む
        • gzipとほぼ同じ理由か
    • 名前がダブったファイルの強制上書き許可は-f,なければ聞かれるまでもなく上書きされない
  • 解凍
    • bunzip2 -k -c [source file path] > [unzipped file path]
    • オプションはそれぞれ,
      • -kで元ファイルを削除しない
      • -cstdoutに指定
    • どうせファイルしか出てこないのでこれで覚えた方が良い
    • 名前がダブったファイルの強制上書き許可は-f,なければ聞かれるまでもなく上書きされない
    • bzip2を入れると勝手にbunzip2もついてくる
      • というよりbzip2を使う上で-zで圧縮するか-dで解凍するかの違いしかない

調べた限りではディレクトリ統合機能もパスワード暗号化機能もなさそう,やはり別ライブラリ(後述)を使うべきだろう.

lrzipの場合

そういやunar使わないんすか?

なんか負けた気分になるし手動で詰めた方が個人的にはスッキリするので…

FireVault

Mac起動ディスクだけならコレで良いと思う
Windowsのbitlockerよりも優秀だと感じている

設定したら復旧キーを忘れないように必ずメモしておこう,iCloud連携は正直やりたくないなって…

外部のボリュームを暗号化

外付けストレージならコレで良いと思う
ディスクユーティリティでexFATフォーマットされたドライブの中にも問答無用で設定できる

veracrypt

Windows向けという印象があるがmacOSでも普通に使える
OSS開発でありながらも,これを使えば暗号化はかなり万全と考えて良さそうか…?

adguardのユーザールールで5ちゃんねる用の非表示フィルタを作成

ワッチョイ,IPアドレスコテハン,名前

!wacchoi,ip,name
5ch.net##div:matches-attr("/class/"="/post|post highlightpost|post_hover vis own[0-9]+/") > div > span:matches-attr("/class/"="/name/"):contains(/ここに正規表現で書き込み/):upward(2):remove()

ワッチョイ決め打ち

正規表現,こんな感じで前後に分けて判定

abcd-....|....-efgh

ID

!userID
5ch.net##div:matches-attr("/class/"="/post|post highlightpost|post_hover vis own[0-9]+/") > div > span:matches-attr("/class/"="/uid/"):contains(/ここに正規表現で書き込み/):upward(2):remove()

末尾の決め打ち

正規表現,例として末尾d・末尾a・末尾r・末尾Mを弾く

\b.+[darM]\b

正規表現の仕様

developer.mozilla.org

概ねこれに一致しているかと思います(オプション指定はしなくても良さそう)

鯖毎or板毎に使い分け

ドメインを細かく指定すれば何とかなると思います

iCloud+の「メールを非公開(偽装アドレス)」で実験してみた

以下,偽装アドレスと呼称します

コレに興味がある方は基本的な使い方は大丈夫だと思われまスゥゥゥ…

https://support.apple.com/ja-jp/HT210425

簡単な説明

macOS Monterey(Public Beta版でそこそこ安定しているやつがあるから登録して落としてこよう
設定 → Apple ID → メールを非公開(オプション)に移動
左下の『➕』で自動生成,ラベルとメモを忘れずに書き加える
アドレスをコピペして実際に使う,以上

用語リスト

f:id:Soluna_Eureka:20211008202622p:plain
説明

偽装アドレス

さっきの手順で手に入れたアドレス
xxx@icloud.comとおく

自分アドレス

自分の本来のiCloudのアドレス
XXX@icloud.comとおく

相手アドレス

やり取りをしたい相手のアドレス
YYY@example.ne.jpとでもおくか

串用アドレス

メールの送信先に実際に置かれるアドレス
4つ偽装アドレスで5つくらい取ってから実験してみた
ユーザーの視点では「プロキシが持つアドレス」に見えるのでそう呼びたい…
YYY@example.ne.jpを用いると
YYY_at_example_ne_jp_[aa][nnnnnnnnnn][bb]_[mm][cccccc]@icloud.comみたいに導出される

[aa][bb][cccccc]は同じ偽装アドレスを用いて通信した相手アドレスであれば共通する
[nnnnnnnnnn](10桁)・[mm](2桁)は相手アドレスによって完全にランダムに振られる
なんでこの桁数や構成になったのかは不明…

実際に制限ルールを探ってみた

ルール1.文通するには最初に相手から送られてくる必要がある

相手アドレスから偽装アドレスにメールが飛んでくると,相手アドレスに対応する串用アドレスが発行され,串用アドレスから自分アドレスにメールが飛んでくる(Applesmtpサーバから転送されてくる)
偽装アドレスを用いて相手アドレスにメールを返信するにはその真逆の動作をしなければならず,自分アドレスから串用アドレスに飛ばすことで偽装アドレスから相手アドレスにメールが飛んでいく

すなわち送信アドレスに対応した串側アドレスをユーザーが叩くことで偽装アドレスから相手アドレスへのアクセスが行えるが,串側アドレスの発行は外側から受けたメールを処理するApplesmtpサーバでしかできない以上,こちらから先制して送りつけることは不可能,対応できる串側アドレスがなければユーザーはなにもできない
濫用防止のためと言われれば当然はであるが

ルール2.串側アドレスの乱数文字列がユーザ認証の役目を果たす

試しに1文字だけ弄って送信したらエラーを吐かれたし,偽装アドレスによって串側アドレスのフォーマットの約半分が定まっていることから,自分アドレスと偽装アドレスの権限照合・串側アドレスと相手アドレスの宛先照合が行われているとみて良さそう
予想するに,他のiCloudユーザが他人の串側アドレスを宛先にして送信してもダメだったりするのではないだろうか…?(誰かやってくれ)

ちなみに宛先が間違っていると代替のsmtpサーバに送信され,Undelivered Mail Returned to Serverされる

ルール3.串側アドレスを宛先に含む場合はアドレス1つ分しか対応しない

宛先に2つ以上の串側アドレスを登録して1度に送ろうとすると,

  • Web版iCloudメールでは警告メッセージを吐かれる
    • 「機能的に無理です!」
  • Macデフォルトのメールソフトでは約5分間隔で宛先1つずつに再送される
    • 「too many recipients!」
      • 代替のstmpサーバの指定を要求されるが,何を選んでも結局は再送になる.
    • なお1つ目の宛先にはすぐに送れる
    • 個別送信扱いになるので他の宛先は見えなくなる

または串側アドレスと通常のアドレスを登録して1度に送ろうとすると,

  • Web版iCloudメールでは警告メッセージを吐かれる
    • 「Undelivered Mail Returned to Server」
    • どっちも届かない
  • Macデフォルトのメールソフトでは約5分間隔で宛先1つずつに再送される
    • 代替のstmpサーバの指定を要求される
      • 選ぶとバラバラに(串側アドレスはApplesmtpサーバで,それ以外は代替smtpサーバで)再送される
      • 相手アドレスから見れば宛先の連名は見れないが,通常のアドレスからは丸見え
    • 1つ目の串側アドレスや通常のアドレスの宛先にはすぐに送れる

つまりメールソフトにおける動作としては,

  1. まずはiCloudサーバにまるごと送る
  2. 串側アドレスがある場合は1つしか受け付けられないと返信する
  3. 代替smtpサーバがない場合は全て破棄,ある場合は
    1. 串側アドレス1つをiCloudサーバに送り
    2. その他全てを代替smtpサーバに送る
    3. 串側アドレスが残れば返送されるので
      1. 約5分間隔で1つずつ再送する
  4. エラーが無くなれば完了

という感じ…っぽい

ルール4.串側アドレスはApple側で保持される

以上のルールを満たすには,偽装アドレスのみならず串側アドレスもAppleが管理していると見るべきだろう
というかリンク切れが起きたりしたらこっちも大変なので,そこら辺は頑張ってセーブし続けてほしい月額160円でやっていいサービスか?これが…

感想

今後また仕様が何かしら変わるかも知れません
が,とりあえず串側アドレスには偽装アドレスと一意に結び付けられる成分があることは頭に入れておくべきだと思いました
万が一の場合にはそれが流出することでやり取りが特定される可能性も考えられます
あとはどれだけ串側アドレスが保持されるのかやってみなくちゃわかりません,1年間の未使用でリセットとかありそうですし…

匿名化ヨシ!ご安全に!

全てのきっかけ

https://twitter.com/Soluna_Eureka/status/1446095138808221707?s=20

Akamai vs Cloudflare vs Fastly : (Private Relayの) 出口プロキシ が NXDOMAIN かどうか

言いたいこと

Apple iCloud+のPrivate Relayは擬似VPNとでも言えるような2段プロキシを採用していますが, 日本の東京エリアではAkamaiとCloudflareとFastlyの3社が分担でが出口プロキシを担当しているようで, これは事前に出さたIPリストからwhoisすることで確認できます.
(本来は地域別のIPアドレス割り当ての対応表リストという役割がありますが,現状ではここ以外から通信することは不可能と思われる以上,これが実質的に出口プロキシのリストそのものになっています.)

同様にそのIPアドレスnslookupを仕掛ければ,Akamaiサーバだけは

a[ppp-qqq-rrr-sss].deploy.static.akamaitechnologies.com
/* [ppp-qqq-rrr-sss] は出口プロキシが利用可能なipv4アドレス */

と解決ますが,CloudflareサーバとFastlyサーバではnxdomainを返されます.

1つの同じサービスにどのような理屈・過程で複数の仕様ができてしまったのか,その理由を知りたいと考えています.
特に一部のサービスではAkamaiサーバにのみ規制がかかっている (5ch.netのburned bbq:proxy(60)など) ため, この (恐らくセキュリティ関係の) 設定の差が不便を生んだのではないかと疑っています.

加えて,Appleは出口プロキシのあり方についてどう考えているのか,DNSレコードに乗らないことをよしとしているのか, または近い将来に仕様が統一されるかユーザーが選択できるか,といったことについても知れたら良いなと思っています.

おまけ

東京の出口プロキシのIPのリスト(CSV)を作ったよ!

一覧

172.224.240.128/27,Akamai
172.225.46.64/26,Akamai
172.225.46.208/28,Akamai
172.225.48.0/26,Akamai
172.225.48.128/28,Akamai
172.225.52.176/28,Akamai
172.225.54.128/26,Akamai
172.225.74.128/26,Akamai
172.225.75.0/24,Akamai
172.225.76.0/24,Akamai
172.225.122.0/25,Akamai
172.225.122.192/27,Akamai
172.225.123.0/25,Akamai
172.225.123.128/27,Akamai
172.225.124.192/27,Akamai
172.225.127.0/25,Akamai
172.226.24.0/25,Akamai
172.226.24.128/27,Akamai
172.226.42.0/25,Akamai
172.226.54.0/26,Akamai
172.226.54.64/28,Akamai
172.226.56.0/26,Akamai
172.226.56.64/28,Akamai
172.226.58.0/26,Akamai
172.226.58.64/28,Akamai
2a02:26f7:b980::/42,Akamai
104.28.0.45/32,Cloudflare
104.28.0.46/32,Cloudflare
104.28.4.73/32,Cloudflare
104.28.4.74/32,Cloudflare
104.28.44.37/32,Cloudflare
104.28.44.38/32,Cloudflare
104.28.44.39/32,Cloudflare
104.28.44.40/32,Cloudflare
104.28.44.41/32,Cloudflare
104.28.44.42/32,Cloudflare
104.28.44.43/32,Cloudflare
104.28.44.44/32,Cloudflare
104.28.44.45/32,Cloudflare
104.28.44.46/32,Cloudflare
104.28.44.47/32,Cloudflare
104.28.44.48/32,Cloudflare
104.28.44.49/32,Cloudflare
104.28.44.50/32,Cloudflare
104.28.44.51/32,Cloudflare
104.28.44.52/32,Cloudflare
104.28.44.53/32,Cloudflare
104.28.44.54/32,Cloudflare
104.28.44.55/32,Cloudflare
104.28.44.56/32,Cloudflare
104.28.44.57/32,Cloudflare
104.28.44.58/32,Cloudflare
104.28.44.59/32,Cloudflare
104.28.44.60/32,Cloudflare
104.28.44.61/32,Cloudflare
104.28.44.62/32,Cloudflare
104.28.44.63/32,Cloudflare
104.28.44.64/32,Cloudflare
104.28.44.65/32,Cloudflare
104.28.44.66/32,Cloudflare
104.28.44.67/32,Cloudflare
104.28.44.68/32,Cloudflare
104.28.44.69/32,Cloudflare
104.28.44.70/32,Cloudflare
104.28.44.71/32,Cloudflare
104.28.44.72/32,Cloudflare
104.28.44.73/32,Cloudflare
104.28.44.74/32,Cloudflare
104.28.67.170/32,Cloudflare
104.28.67.171/32,Cloudflare
104.28.67.172/32,Cloudflare
104.28.67.173/32,Cloudflare
104.28.67.174/32,Cloudflare
104.28.67.175/32,Cloudflare
104.28.67.176/32,Cloudflare
104.28.67.177/32,Cloudflare
104.28.67.178/32,Cloudflare
104.28.67.179/32,Cloudflare
104.28.67.180/32,Cloudflare
104.28.67.181/32,Cloudflare
104.28.67.182/32,Cloudflare
104.28.67.183/32,Cloudflare
104.28.67.184/32,Cloudflare
104.28.67.185/32,Cloudflare
104.28.67.186/32,Cloudflare
104.28.67.187/32,Cloudflare
104.28.67.188/32,Cloudflare
104.28.67.189/32,Cloudflare
104.28.67.190/32,Cloudflare
104.28.67.191/32,Cloudflare
104.28.67.192/32,Cloudflare
104.28.67.193/32,Cloudflare
104.28.67.194/32,Cloudflare
104.28.67.195/32,Cloudflare
104.28.67.196/32,Cloudflare
104.28.67.197/32,Cloudflare
104.28.67.198/32,Cloudflare
104.28.67.199/32,Cloudflare
104.28.67.200/32,Cloudflare
104.28.67.201/32,Cloudflare
104.28.67.202/32,Cloudflare
104.28.67.203/32,Cloudflare
104.28.67.204/32,Cloudflare
104.28.67.205/32,Cloudflare
104.28.67.206/32,Cloudflare
104.28.67.207/32,Cloudflare
104.28.70.170/32,Cloudflare
104.28.70.171/32,Cloudflare
104.28.70.172/32,Cloudflare
104.28.70.173/32,Cloudflare
104.28.70.174/32,Cloudflare
104.28.70.175/32,Cloudflare
104.28.70.176/32,Cloudflare
104.28.70.177/32,Cloudflare
104.28.70.178/32,Cloudflare
104.28.70.179/32,Cloudflare
104.28.70.180/32,Cloudflare
104.28.70.181/32,Cloudflare
104.28.70.182/32,Cloudflare
104.28.70.183/32,Cloudflare
104.28.70.184/32,Cloudflare
104.28.70.185/32,Cloudflare
104.28.70.186/32,Cloudflare
104.28.70.187/32,Cloudflare
104.28.70.188/32,Cloudflare
104.28.70.189/32,Cloudflare
104.28.70.190/32,Cloudflare
104.28.70.191/32,Cloudflare
104.28.70.192/32,Cloudflare
104.28.70.193/32,Cloudflare
104.28.70.194/32,Cloudflare
104.28.70.195/32,Cloudflare
104.28.70.196/32,Cloudflare
104.28.70.197/32,Cloudflare
104.28.70.198/32,Cloudflare
104.28.70.199/32,Cloudflare
104.28.70.200/32,Cloudflare
104.28.70.201/32,Cloudflare
104.28.70.202/32,Cloudflare
104.28.70.203/32,Cloudflare
104.28.70.204/32,Cloudflare
104.28.70.205/32,Cloudflare
104.28.70.206/32,Cloudflare
104.28.70.207/32,Cloudflare
104.28.83.195/32,Cloudflare
104.28.83.196/32,Cloudflare
104.28.83.197/32,Cloudflare
104.28.83.198/32,Cloudflare
104.28.83.199/32,Cloudflare
104.28.83.200/32,Cloudflare
104.28.83.201/32,Cloudflare
104.28.83.202/32,Cloudflare
104.28.83.203/32,Cloudflare
104.28.83.204/32,Cloudflare
104.28.83.205/32,Cloudflare
104.28.83.206/32,Cloudflare
104.28.83.207/32,Cloudflare
104.28.83.208/32,Cloudflare
104.28.83.209/32,Cloudflare
104.28.83.210/32,Cloudflare
104.28.83.211/32,Cloudflare
104.28.83.212/32,Cloudflare
104.28.83.213/32,Cloudflare
104.28.83.214/32,Cloudflare
104.28.83.215/32,Cloudflare
104.28.83.216/32,Cloudflare
104.28.83.217/32,Cloudflare
104.28.83.218/32,Cloudflare
104.28.83.219/32,Cloudflare
104.28.83.220/32,Cloudflare
104.28.83.221/32,Cloudflare
104.28.83.222/32,Cloudflare
104.28.83.223/32,Cloudflare
104.28.83.224/32,Cloudflare
104.28.83.225/32,Cloudflare
104.28.83.226/32,Cloudflare
104.28.83.227/32,Cloudflare
104.28.83.228/32,Cloudflare
104.28.83.229/32,Cloudflare
104.28.83.230/32,Cloudflare
104.28.83.231/32,Cloudflare
104.28.83.232/32,Cloudflare
104.28.99.191/32,Cloudflare
104.28.99.192/32,Cloudflare
104.28.99.193/32,Cloudflare
104.28.99.194/32,Cloudflare
104.28.99.195/32,Cloudflare
104.28.99.196/32,Cloudflare
104.28.99.197/32,Cloudflare
104.28.99.198/32,Cloudflare
104.28.99.199/32,Cloudflare
104.28.99.200/32,Cloudflare
104.28.99.201/32,Cloudflare
104.28.99.202/32,Cloudflare
104.28.99.203/32,Cloudflare
104.28.99.204/32,Cloudflare
104.28.99.205/32,Cloudflare
104.28.99.206/32,Cloudflare
104.28.99.207/32,Cloudflare
104.28.99.208/32,Cloudflare
104.28.99.209/32,Cloudflare
104.28.99.210/32,Cloudflare
104.28.99.211/32,Cloudflare
104.28.99.212/32,Cloudflare
104.28.99.213/32,Cloudflare
104.28.99.214/32,Cloudflare
104.28.99.215/32,Cloudflare
104.28.99.216/32,Cloudflare
104.28.99.217/32,Cloudflare
104.28.99.218/32,Cloudflare
104.28.99.219/32,Cloudflare
104.28.99.220/32,Cloudflare
104.28.99.221/32,Cloudflare
104.28.99.222/32,Cloudflare
104.28.99.223/32,Cloudflare
104.28.99.224/32,Cloudflare
104.28.99.225/32,Cloudflare
104.28.99.226/32,Cloudflare
104.28.99.227/32,Cloudflare
104.28.99.228/32,Cloudflare
104.28.101.191/32,Cloudflare
104.28.101.192/32,Cloudflare
104.28.101.193/32,Cloudflare
104.28.101.194/32,Cloudflare
104.28.101.195/32,Cloudflare
104.28.101.196/32,Cloudflare
104.28.101.197/32,Cloudflare
104.28.101.198/32,Cloudflare
104.28.101.199/32,Cloudflare
104.28.101.200/32,Cloudflare
104.28.101.201/32,Cloudflare
104.28.101.202/32,Cloudflare
104.28.101.203/32,Cloudflare
104.28.101.204/32,Cloudflare
104.28.101.205/32,Cloudflare
104.28.101.206/32,Cloudflare
104.28.101.207/32,Cloudflare
104.28.101.208/32,Cloudflare
104.28.101.209/32,Cloudflare
104.28.101.210/32,Cloudflare
104.28.101.211/32,Cloudflare
104.28.101.212/32,Cloudflare
104.28.101.213/32,Cloudflare
104.28.101.214/32,Cloudflare
104.28.101.215/32,Cloudflare
104.28.101.216/32,Cloudflare
104.28.101.217/32,Cloudflare
104.28.101.218/32,Cloudflare
104.28.101.219/32,Cloudflare
104.28.101.220/32,Cloudflare
104.28.101.221/32,Cloudflare
104.28.101.222/32,Cloudflare
104.28.101.223/32,Cloudflare
104.28.101.224/32,Cloudflare
104.28.101.225/32,Cloudflare
104.28.101.226/32,Cloudflare
104.28.101.227/32,Cloudflare
104.28.101.228/32,Cloudflare
104.28.118.164/32,Cloudflare
104.28.118.165/32,Cloudflare
104.28.118.166/32,Cloudflare
104.28.118.167/32,Cloudflare
104.28.118.168/32,Cloudflare
104.28.118.169/32,Cloudflare
104.28.118.170/32,Cloudflare
104.28.118.171/32,Cloudflare
104.28.118.172/32,Cloudflare
104.28.118.173/32,Cloudflare
104.28.118.174/32,Cloudflare
104.28.118.175/32,Cloudflare
104.28.118.176/32,Cloudflare
104.28.118.177/32,Cloudflare
104.28.118.178/32,Cloudflare
104.28.118.179/32,Cloudflare
104.28.118.180/32,Cloudflare
104.28.118.181/32,Cloudflare
104.28.118.182/32,Cloudflare
104.28.118.183/32,Cloudflare
104.28.118.184/32,Cloudflare
104.28.118.185/32,Cloudflare
104.28.118.186/32,Cloudflare
104.28.118.187/32,Cloudflare
104.28.118.188/32,Cloudflare
104.28.118.189/32,Cloudflare
104.28.118.190/32,Cloudflare
104.28.118.191/32,Cloudflare
104.28.118.192/32,Cloudflare
104.28.118.193/32,Cloudflare
104.28.118.194/32,Cloudflare
104.28.118.195/32,Cloudflare
104.28.118.196/32,Cloudflare
104.28.118.197/32,Cloudflare
104.28.118.198/32,Cloudflare
104.28.118.199/32,Cloudflare
104.28.118.200/32,Cloudflare
104.28.118.201/32,Cloudflare
104.28.121.164/32,Cloudflare
104.28.121.165/32,Cloudflare
104.28.121.166/32,Cloudflare
104.28.121.167/32,Cloudflare
104.28.121.168/32,Cloudflare
104.28.121.169/32,Cloudflare
104.28.121.170/32,Cloudflare
104.28.121.171/32,Cloudflare
104.28.121.172/32,Cloudflare
104.28.121.173/32,Cloudflare
104.28.121.174/32,Cloudflare
104.28.121.175/32,Cloudflare
104.28.121.176/32,Cloudflare
104.28.121.177/32,Cloudflare
104.28.121.178/32,Cloudflare
104.28.121.179/32,Cloudflare
104.28.121.180/32,Cloudflare
104.28.121.181/32,Cloudflare
104.28.121.182/32,Cloudflare
104.28.121.183/32,Cloudflare
104.28.121.184/32,Cloudflare
104.28.121.185/32,Cloudflare
104.28.121.186/32,Cloudflare
104.28.121.187/32,Cloudflare
104.28.121.188/32,Cloudflare
104.28.121.189/32,Cloudflare
104.28.121.190/32,Cloudflare
104.28.121.191/32,Cloudflare
104.28.121.192/32,Cloudflare
104.28.121.193/32,Cloudflare
104.28.121.194/32,Cloudflare
104.28.121.195/32,Cloudflare
104.28.121.196/32,Cloudflare
104.28.121.197/32,Cloudflare
104.28.121.198/32,Cloudflare
104.28.121.199/32,Cloudflare
104.28.121.200/32,Cloudflare
104.28.121.201/32,Cloudflare
2606:54c0:3b00:10::/64,Cloudflare
2606:54c0:3b00:128::/64,Cloudflare
2606:54c0:3b20:10::/64,Cloudflare
2606:54c0:3b20:128::/64,Cloudflare
2606:54c0:3b40:10::/64,Cloudflare
2606:54c0:3b40:128::/64,Cloudflare
2606:54c0:3b60:10::/64,Cloudflare
104.28.0.45/33,Cloudflare
146.75.189.22/31,Fastly
2a04:4e41:0029:000b::/64,Fastly
146.75.196.14/31,Fastly
2a04:4e41:0030:0007::/64,Fastly
146.75.201.12/31,Fastly
2a04:4e41:0035:0006::/64,Fastly

みんなも試してみよう!

原文

Akamai vs Cloudflare vs Fastly : whether Egress Proxy (on Private Relay) are NXDOMAIN

In my country (Tokyo, Japan), it seems like that

"Akamai Technologies, Inc.", "Cloudflare, Inc.", and "Fastly, Inc.",

are providing almost all the egress proxy for using Private Relay (and these are estimated by whois information).

Also I was able to resolve the domain of Akamai's egress proxy with nslookup, such as:

a[ppp-qqq-rrr-sss].deploy.static.akamaitechnologies.com
/* [ppp-qqq-rrr-sss] means available ipv4 address of egress proxy */

but I couldn't resolve Cloudflare's and Fastly's, with NXDOMAIN error.

I want to know how and why has this difference occurred between Cloudflare, Fastly and Akamai, still it's one and the same service.

Because I found that some web services limit the use for accesses from Akami's Egress Proxy only, thus I'm guessing that this difference (security settings?) is causing the inconvenience.

In addition, I would like to know if Apple is comfortable with the situation of egress proxy not in the DNS records. And I hope that near the future either specification would be standardized or users would be able to choose them.


By the way, I might be often assigned the following IP addresses:

the list

172.224.240.128/27,Akamai
172.225.46.64/26,Akamai
172.225.46.208/28,Akamai
172.225.48.0/26,Akamai
172.225.48.128/28,Akamai
172.225.52.176/28,Akamai
172.225.54.128/26,Akamai
172.225.74.128/26,Akamai
172.225.75.0/24,Akamai
172.225.76.0/24,Akamai
172.225.122.0/25,Akamai
172.225.122.192/27,Akamai
172.225.123.0/25,Akamai
172.225.123.128/27,Akamai
172.225.124.192/27,Akamai
172.225.127.0/25,Akamai
172.226.24.0/25,Akamai
172.226.24.128/27,Akamai
172.226.42.0/25,Akamai
172.226.54.0/26,Akamai
172.226.54.64/28,Akamai
172.226.56.0/26,Akamai
172.226.56.64/28,Akamai
172.226.58.0/26,Akamai
172.226.58.64/28,Akamai
2a02:26f7:b980::/42,Akamai
104.28.0.45/32,Cloudflare
104.28.0.46/32,Cloudflare
104.28.4.73/32,Cloudflare
104.28.4.74/32,Cloudflare
104.28.44.37/32,Cloudflare
104.28.44.38/32,Cloudflare
104.28.44.39/32,Cloudflare
104.28.44.40/32,Cloudflare
104.28.44.41/32,Cloudflare
104.28.44.42/32,Cloudflare
104.28.44.43/32,Cloudflare
104.28.44.44/32,Cloudflare
104.28.44.45/32,Cloudflare
104.28.44.46/32,Cloudflare
104.28.44.47/32,Cloudflare
104.28.44.48/32,Cloudflare
104.28.44.49/32,Cloudflare
104.28.44.50/32,Cloudflare
104.28.44.51/32,Cloudflare
104.28.44.52/32,Cloudflare
104.28.44.53/32,Cloudflare
104.28.44.54/32,Cloudflare
104.28.44.55/32,Cloudflare
104.28.44.56/32,Cloudflare
104.28.44.57/32,Cloudflare
104.28.44.58/32,Cloudflare
104.28.44.59/32,Cloudflare
104.28.44.60/32,Cloudflare
104.28.44.61/32,Cloudflare
104.28.44.62/32,Cloudflare
104.28.44.63/32,Cloudflare
104.28.44.64/32,Cloudflare
104.28.44.65/32,Cloudflare
104.28.44.66/32,Cloudflare
104.28.44.67/32,Cloudflare
104.28.44.68/32,Cloudflare
104.28.44.69/32,Cloudflare
104.28.44.70/32,Cloudflare
104.28.44.71/32,Cloudflare
104.28.44.72/32,Cloudflare
104.28.44.73/32,Cloudflare
104.28.44.74/32,Cloudflare
104.28.67.170/32,Cloudflare
104.28.67.171/32,Cloudflare
104.28.67.172/32,Cloudflare
104.28.67.173/32,Cloudflare
104.28.67.174/32,Cloudflare
104.28.67.175/32,Cloudflare
104.28.67.176/32,Cloudflare
104.28.67.177/32,Cloudflare
104.28.67.178/32,Cloudflare
104.28.67.179/32,Cloudflare
104.28.67.180/32,Cloudflare
104.28.67.181/32,Cloudflare
104.28.67.182/32,Cloudflare
104.28.67.183/32,Cloudflare
104.28.67.184/32,Cloudflare
104.28.67.185/32,Cloudflare
104.28.67.186/32,Cloudflare
104.28.67.187/32,Cloudflare
104.28.67.188/32,Cloudflare
104.28.67.189/32,Cloudflare
104.28.67.190/32,Cloudflare
104.28.67.191/32,Cloudflare
104.28.67.192/32,Cloudflare
104.28.67.193/32,Cloudflare
104.28.67.194/32,Cloudflare
104.28.67.195/32,Cloudflare
104.28.67.196/32,Cloudflare
104.28.67.197/32,Cloudflare
104.28.67.198/32,Cloudflare
104.28.67.199/32,Cloudflare
104.28.67.200/32,Cloudflare
104.28.67.201/32,Cloudflare
104.28.67.202/32,Cloudflare
104.28.67.203/32,Cloudflare
104.28.67.204/32,Cloudflare
104.28.67.205/32,Cloudflare
104.28.67.206/32,Cloudflare
104.28.67.207/32,Cloudflare
104.28.70.170/32,Cloudflare
104.28.70.171/32,Cloudflare
104.28.70.172/32,Cloudflare
104.28.70.173/32,Cloudflare
104.28.70.174/32,Cloudflare
104.28.70.175/32,Cloudflare
104.28.70.176/32,Cloudflare
104.28.70.177/32,Cloudflare
104.28.70.178/32,Cloudflare
104.28.70.179/32,Cloudflare
104.28.70.180/32,Cloudflare
104.28.70.181/32,Cloudflare
104.28.70.182/32,Cloudflare
104.28.70.183/32,Cloudflare
104.28.70.184/32,Cloudflare
104.28.70.185/32,Cloudflare
104.28.70.186/32,Cloudflare
104.28.70.187/32,Cloudflare
104.28.70.188/32,Cloudflare
104.28.70.189/32,Cloudflare
104.28.70.190/32,Cloudflare
104.28.70.191/32,Cloudflare
104.28.70.192/32,Cloudflare
104.28.70.193/32,Cloudflare
104.28.70.194/32,Cloudflare
104.28.70.195/32,Cloudflare
104.28.70.196/32,Cloudflare
104.28.70.197/32,Cloudflare
104.28.70.198/32,Cloudflare
104.28.70.199/32,Cloudflare
104.28.70.200/32,Cloudflare
104.28.70.201/32,Cloudflare
104.28.70.202/32,Cloudflare
104.28.70.203/32,Cloudflare
104.28.70.204/32,Cloudflare
104.28.70.205/32,Cloudflare
104.28.70.206/32,Cloudflare
104.28.70.207/32,Cloudflare
104.28.83.195/32,Cloudflare
104.28.83.196/32,Cloudflare
104.28.83.197/32,Cloudflare
104.28.83.198/32,Cloudflare
104.28.83.199/32,Cloudflare
104.28.83.200/32,Cloudflare
104.28.83.201/32,Cloudflare
104.28.83.202/32,Cloudflare
104.28.83.203/32,Cloudflare
104.28.83.204/32,Cloudflare
104.28.83.205/32,Cloudflare
104.28.83.206/32,Cloudflare
104.28.83.207/32,Cloudflare
104.28.83.208/32,Cloudflare
104.28.83.209/32,Cloudflare
104.28.83.210/32,Cloudflare
104.28.83.211/32,Cloudflare
104.28.83.212/32,Cloudflare
104.28.83.213/32,Cloudflare
104.28.83.214/32,Cloudflare
104.28.83.215/32,Cloudflare
104.28.83.216/32,Cloudflare
104.28.83.217/32,Cloudflare
104.28.83.218/32,Cloudflare
104.28.83.219/32,Cloudflare
104.28.83.220/32,Cloudflare
104.28.83.221/32,Cloudflare
104.28.83.222/32,Cloudflare
104.28.83.223/32,Cloudflare
104.28.83.224/32,Cloudflare
104.28.83.225/32,Cloudflare
104.28.83.226/32,Cloudflare
104.28.83.227/32,Cloudflare
104.28.83.228/32,Cloudflare
104.28.83.229/32,Cloudflare
104.28.83.230/32,Cloudflare
104.28.83.231/32,Cloudflare
104.28.83.232/32,Cloudflare
104.28.99.191/32,Cloudflare
104.28.99.192/32,Cloudflare
104.28.99.193/32,Cloudflare
104.28.99.194/32,Cloudflare
104.28.99.195/32,Cloudflare
104.28.99.196/32,Cloudflare
104.28.99.197/32,Cloudflare
104.28.99.198/32,Cloudflare
104.28.99.199/32,Cloudflare
104.28.99.200/32,Cloudflare
104.28.99.201/32,Cloudflare
104.28.99.202/32,Cloudflare
104.28.99.203/32,Cloudflare
104.28.99.204/32,Cloudflare
104.28.99.205/32,Cloudflare
104.28.99.206/32,Cloudflare
104.28.99.207/32,Cloudflare
104.28.99.208/32,Cloudflare
104.28.99.209/32,Cloudflare
104.28.99.210/32,Cloudflare
104.28.99.211/32,Cloudflare
104.28.99.212/32,Cloudflare
104.28.99.213/32,Cloudflare
104.28.99.214/32,Cloudflare
104.28.99.215/32,Cloudflare
104.28.99.216/32,Cloudflare
104.28.99.217/32,Cloudflare
104.28.99.218/32,Cloudflare
104.28.99.219/32,Cloudflare
104.28.99.220/32,Cloudflare
104.28.99.221/32,Cloudflare
104.28.99.222/32,Cloudflare
104.28.99.223/32,Cloudflare
104.28.99.224/32,Cloudflare
104.28.99.225/32,Cloudflare
104.28.99.226/32,Cloudflare
104.28.99.227/32,Cloudflare
104.28.99.228/32,Cloudflare
104.28.101.191/32,Cloudflare
104.28.101.192/32,Cloudflare
104.28.101.193/32,Cloudflare
104.28.101.194/32,Cloudflare
104.28.101.195/32,Cloudflare
104.28.101.196/32,Cloudflare
104.28.101.197/32,Cloudflare
104.28.101.198/32,Cloudflare
104.28.101.199/32,Cloudflare
104.28.101.200/32,Cloudflare
104.28.101.201/32,Cloudflare
104.28.101.202/32,Cloudflare
104.28.101.203/32,Cloudflare
104.28.101.204/32,Cloudflare
104.28.101.205/32,Cloudflare
104.28.101.206/32,Cloudflare
104.28.101.207/32,Cloudflare
104.28.101.208/32,Cloudflare
104.28.101.209/32,Cloudflare
104.28.101.210/32,Cloudflare
104.28.101.211/32,Cloudflare
104.28.101.212/32,Cloudflare
104.28.101.213/32,Cloudflare
104.28.101.214/32,Cloudflare
104.28.101.215/32,Cloudflare
104.28.101.216/32,Cloudflare
104.28.101.217/32,Cloudflare
104.28.101.218/32,Cloudflare
104.28.101.219/32,Cloudflare
104.28.101.220/32,Cloudflare
104.28.101.221/32,Cloudflare
104.28.101.222/32,Cloudflare
104.28.101.223/32,Cloudflare
104.28.101.224/32,Cloudflare
104.28.101.225/32,Cloudflare
104.28.101.226/32,Cloudflare
104.28.101.227/32,Cloudflare
104.28.101.228/32,Cloudflare
104.28.118.164/32,Cloudflare
104.28.118.165/32,Cloudflare
104.28.118.166/32,Cloudflare
104.28.118.167/32,Cloudflare
104.28.118.168/32,Cloudflare
104.28.118.169/32,Cloudflare
104.28.118.170/32,Cloudflare
104.28.118.171/32,Cloudflare
104.28.118.172/32,Cloudflare
104.28.118.173/32,Cloudflare
104.28.118.174/32,Cloudflare
104.28.118.175/32,Cloudflare
104.28.118.176/32,Cloudflare
104.28.118.177/32,Cloudflare
104.28.118.178/32,Cloudflare
104.28.118.179/32,Cloudflare
104.28.118.180/32,Cloudflare
104.28.118.181/32,Cloudflare
104.28.118.182/32,Cloudflare
104.28.118.183/32,Cloudflare
104.28.118.184/32,Cloudflare
104.28.118.185/32,Cloudflare
104.28.118.186/32,Cloudflare
104.28.118.187/32,Cloudflare
104.28.118.188/32,Cloudflare
104.28.118.189/32,Cloudflare
104.28.118.190/32,Cloudflare
104.28.118.191/32,Cloudflare
104.28.118.192/32,Cloudflare
104.28.118.193/32,Cloudflare
104.28.118.194/32,Cloudflare
104.28.118.195/32,Cloudflare
104.28.118.196/32,Cloudflare
104.28.118.197/32,Cloudflare
104.28.118.198/32,Cloudflare
104.28.118.199/32,Cloudflare
104.28.118.200/32,Cloudflare
104.28.118.201/32,Cloudflare
104.28.121.164/32,Cloudflare
104.28.121.165/32,Cloudflare
104.28.121.166/32,Cloudflare
104.28.121.167/32,Cloudflare
104.28.121.168/32,Cloudflare
104.28.121.169/32,Cloudflare
104.28.121.170/32,Cloudflare
104.28.121.171/32,Cloudflare
104.28.121.172/32,Cloudflare
104.28.121.173/32,Cloudflare
104.28.121.174/32,Cloudflare
104.28.121.175/32,Cloudflare
104.28.121.176/32,Cloudflare
104.28.121.177/32,Cloudflare
104.28.121.178/32,Cloudflare
104.28.121.179/32,Cloudflare
104.28.121.180/32,Cloudflare
104.28.121.181/32,Cloudflare
104.28.121.182/32,Cloudflare
104.28.121.183/32,Cloudflare
104.28.121.184/32,Cloudflare
104.28.121.185/32,Cloudflare
104.28.121.186/32,Cloudflare
104.28.121.187/32,Cloudflare
104.28.121.188/32,Cloudflare
104.28.121.189/32,Cloudflare
104.28.121.190/32,Cloudflare
104.28.121.191/32,Cloudflare
104.28.121.192/32,Cloudflare
104.28.121.193/32,Cloudflare
104.28.121.194/32,Cloudflare
104.28.121.195/32,Cloudflare
104.28.121.196/32,Cloudflare
104.28.121.197/32,Cloudflare
104.28.121.198/32,Cloudflare
104.28.121.199/32,Cloudflare
104.28.121.200/32,Cloudflare
104.28.121.201/32,Cloudflare
2606:54c0:3b00:10::/64,Cloudflare
2606:54c0:3b00:128::/64,Cloudflare
2606:54c0:3b20:10::/64,Cloudflare
2606:54c0:3b20:128::/64,Cloudflare
2606:54c0:3b40:10::/64,Cloudflare
2606:54c0:3b40:128::/64,Cloudflare
2606:54c0:3b60:10::/64,Cloudflare
104.28.0.45/33,Cloudflare
146.75.189.22/31,Fastly
2a04:4e41:0029:000b::/64,Fastly
146.75.196.14/31,Fastly
2a04:4e41:0030:0007::/64,Fastly
146.75.201.12/31,Fastly
2a04:4e41:0035:0006::/64,Fastly

so now you can check it.

Firefox Focus + Private Relay + kiriwake jpneの挙動

さて!答えを予想してみましょう!

実験1

iOS 15もしくはiPadOS15において,以下の2つのブラウザ

で以下の3つのサイト

を覗いた時,対応するプロバイダ(もしくはそれが十分に類推できるiPアドレス)が以下のように表示された.

Safari Firefox Forcus
kiriwake jpne cman cgi test-ipv6 kiriwake jpne cman cgi test-ipv6
Private Relay enable v6+ egress node egress node egress node egress node jpne vne jpne vne
4G LTE egress node egress node egress node egress node docomo sp docomo sp
Private Relay disable v6+ jpne vne
4G LTE docomo sp

ここでdocomo sp(spmode-ne.jp)の場合は必ずipv4接続のみが表示された.
egress nodeはiCloud+のPrivate Relay出口ノード(プロキシ?サーバ?)であり,Fastly・Cloudflare・Akamaiのいずれかがランダムに出現する.

考察1-1

AppleによるPrivate Relayの説明によれば,

https://support.apple.com/ja-jp/HT212614

iCloud+ のサブスクリプションで使える iCloud プライベートリレーは、Safari で Web を閲覧する際にプライバシーを守ってくれます。詳しくご説明します。

https://developer.apple.com/jp/support/prepare-your-network-for-icloud-private-relay/

Private Relayは、SafariでのWebブラウジングDNS解決クエリを保護し、Appの安全でないhttpトラフィックからユーザーを守ります。

https://www.apple.com/jp/newsroom/2021/06/apple-advances-its-privacy-leadership-with-ios-15-ipados-15-macos-monterey-and-watchos-8/

Safariでのブラウズ時、Private Relayはユーザーのデバイスから発信されるすべてのトラフィックを確実に暗号化し、Appleやユーザーのネットワークプロバイダを含む何者もユーザーと訪問先のウェブサイトの間でのトラフィックにアクセスしたり読み取ったりできないようにします。

とあることから,Safari(又はSFSafariViewController系)ではないはずのFirefox Focusで接続した時にPrivate RelayのEgress Nodeの情報が表示されたkiriwake jpneの挙動は『この説明には』則していないと言える.

現にこうやって表示された以上,何らかの干渉が発生していると見るべきでは?

考察1-2

kiriwake jpneについて解説するページがjpneからお出しされていたが,

https://www.jpne.co.jp/ebooks/v6plus-ebook.pdf

なお、以下の状況では、JPNE切り分けサイトによるチェックが失敗する可能性があるので注意が必要です。

Google Chromeのライトモード(GoogleのProxy経由)がON Cloudflare WARPなどのVPNが設定されていた

上記状況では、JPNE切り分けサイトとの接続がプロキシ経由になります。このとき、HTTPなどによってJPNE切り分けサイトと直接接続するのはプロキシです。 そのため、JPNE切り分けサイトのチェックで、v6プラスを利用していないと判定されてしまいます。

確かにSafariなら説明の通りの挙動をするものの,Private Relayの対象外であるはずのFirefox Focusでこの挙動をする説明には至っていない.
ページのソースを見ても,jpne.co.jpがこちらのipv6アドレスを把握してページをレンダリングしてから.cgiファイルを送ってきているように見える.

推察1-1

もしかして…ipv6接続が優先されるサイトの場合,ブラウザがSafariかどうかに関わらず,強制的にPrivate Relayを経由するようになる…とか?

追加実験1

nslookup kiriwake.jpne.co.jpで得られた18.179.181.47でアクセスしてみた,これを叩くと強制的にipv4接続になり,トップにipv4アドレスが表示される
…ダメです,出口串のIPアドレスが出ました,結果もさっきと変わらなかったです

追加実験2

iPadOSで同じことをやったらiOSと同じ結果になった,これは一体…

推察1-2

なぜかkiriwake.jpne.co.jpとPrivate RelayとFirefox Focusだけ相性が悪いとしか言いようがない

実験2

他のサイトでもやってみたり,パケット覗き見したりする必要があると思いましたので,お時間を下さい
先駆者の方がいらっしゃいました,これはありがたい…

解決

Appの安全でないhttpトラフィックからユーザーを守ります。

この「App」は「Safariだけ」ではなく「App全体」を指す
すなわち

  • 端末から出るTCP80宛の通信
  • WKWebViewから出るHTTPプロトコルの通信
  • SafariおよびSFSafariViewControllerから出る通信

が対象となり,上記の3サイトの中でkiriwake jpneのURLだけが非httpsであったため,Firefox Focusと言えどPrivate Relayに通信を流さざるを得なかったという流れになる.
もちろん当該サイトはhttpsでも実装されているので,つまり「なぜかhttpでurlを生成して実験した私が悪い」という表現が正確だと言えるだろう.う〜んこの

この辺はOS側で通信をチェックしているようで,非httpsやTCP80宛の通信Chromeでも何でもPrivate Relayへと流される.
実際にやってみたら本当にそうなった,端末から見えちゃうくらいなら隠せというAppleの思想なんだろうか…?

感想

macOS Montereyが正式リリースされ次第でそっちでも確認したいですけど,誰かにこれと同じことを試してみて欲しいと思っています…

実際の検証を元に教えて頂いた方,本当にありがとうございます🙏↓
https://twitter.com/falms

Apple (iCloud+) Private Relay (iOS15~)ついに来たな…

まずは公式サイトを読もう

www.apple.com developer.apple.com support.apple.com

利用環境

iOS 15, iPad OS 15,macOS MontereyにおけるSafari Appおよびアプリ内のSafariブラウザ
(それに加えてSFSafariViewControllerで動いてる画面)

developer.apple.com

他社製のブラウザやWKWebViewで動いている画面,およびその他の通信方法を提供するブラウザは対象外
マシン全体の通信の置き換えはできない,適用範囲はあくまでSafari系の周りにのみ限られる
および,端末から出るTCP80宛の通信WKWebViewから出るHTTPプロトコルの通信全て
(以下に経緯が載っています)

soluna-eureka.hatenablog.com

WKWebViewとSFSafariViewControllerの見分け方

右上か左上に「ぁあ」があればSFSafariViewController,「⬆️」しかなければWKWebView
後者もしくは独自ブラウザで行うTCP443宛のHTTPS通信なら,Private Relayにはまず流されないと考えても良さそう

出口サーバの管轄会社はどこ?

cloudflare, akamai, fastlyの3社…らしい(他にあったら教えて欲しい),ここら辺はAppleが一括で契約してくれてるはず

www.cman.jp

ここから見て自分のIPアドレスの登録情報が上の3社のどれかなら成功

www.speedtest.net

もしくはここで「iCloud Private Relay」と出れば成功

契約方法

既にiCloud+の一部になっているので,iCloudに課金契約をつけてiCloud+にしてやる必要がある, つまりは無料Apple会員では利用不可,当たり前だよなぁ?

support.apple.com

最安なら50GBを130円で購入できるので,ライトユーザーはコレくらいでいいっすね

利用方法

  1. 契約します
  2. 本体を買ってApple IDでログインします
  3. 「設定」→「Apple ID」→「iCloud」→「プライベートリレー(ベータ版)」
  4. オンにする
  5. IPアドレス位置情報設定」をいじる(「国と時間帯を使用」で良いんじゃないかな)

不都合が起きれば別のブラウザを使えば良いしオフにしても良い,割と簡単に素早くスイッチが入るっぽいのでご安心を

仕組み

Private RelayはiCloudに組み込まれている新しいインターネットプライバシーサービスで、ユーザーはより安全でプライバシーが保護された方法でウェブに接続してブラウズできるようになります。Safariでのブラウズ時、Private Relayはユーザーのデバイスから発信されるすべてのトラフィックを確実に暗号化し、Appleやユーザーのネットワークプロバイダを含む何者もユーザーと訪問先のウェブサイトの間でのトラフィックにアクセスしたり読み取ったりできないようにします。その後、ユーザーのリクエストはすべて2つの別々のインターネットリレーを通じて送信されます。1つ目はユーザーの実際の位置情報ではなく地域に割り当てられた匿名のIPアドレスをユーザーに割り当てます。2つ目はユーザーが訪問しようとしているウェブアドレスを復号し、目的の場所にユーザーを転送します。この情報の分割によって、ユーザーと訪問先のサイトの両方が、Appleを含むどのような組織にも特定できなくなるため、ユーザーのプライバシーが保護されます。
https://www.apple.com/jp/newsroom/2021/06/apple-advances-its-privacy-leadership-with-ios-15-ipados-15-macos-monterey-and-watchos-8/

iCloud Private Relayサービスは、革新的なマルチホップアーキテクチャを採用しています。異なる事業者が運用する2つの独立したインターネットリレーを介してリクエストが送信されるため、Appleを含むいかなる者も、ユーザーのブラウジングアクティビティの詳細を閲覧したり収集したりすることができません。Private Relayでは、接続しているクライアントがiPhoneiPadMacであることが検証されるため、接続元のデバイスAppleバイスであることが保証されます。また、デフォルトでは、エグレスIPアドレスにより、クライアントのおおよその位置情報が都市レベルで正確に表されるので、IPアドレスに基づいて地域ベースの制限を課す際に、関連する位置情報をネットワークが受け取ることもできます。その場合でもクライアントの固有IPアドレスは引き続きマスク処理され、匿名化されたアドレスのみがWebサイトと共有されます。これらのアドレスは、Private Relayユーザーのグループ間でも共有されます。
https://developer.apple.com/jp/support/prepare-your-network-for-icloud-private-relay/

通常は、Web を閲覧すると、Web トラフィックに含まれている情報 (DNS レコードや IP アドレスなど) をネットワークのプロバイダや、閲覧した Web サイトに知られてしまいます。この情報を基に、閲覧者の本人確認が実施されたり、時間経過に伴う位置情報や閲覧履歴を蓄積してプロフィールが作成されたりする場合があります。iCloud プライベートリレーは、Safari で Web を閲覧する際に、あなたが誰で、どのサイトを訪れているのか、Apple も含め誰一人としてわからないよう徹底し、プライバシーを守るしくみになっています。
プライベートリレーが有効になっている場合、あなたのリクエストは 2 つの個別の安全なインターネットリレーに分けて送られます。IP アドレスを見ることができるのは、ネットワークのプロバイダと 1 つ目のリレー、これは Apple が運用しています。DNS レコードは暗号化されるので、あなたが閲覧しようとしている Web サイトのアドレスを両者とも知ることはできません。2 つ目のリレーは他社のコンテンツプロバイダが運用していて、これが一時的な IP アドレスを生成し、あなたがリクエストした Web サイトの名前を復号化した上で、そのサイトにあなたをつないでくれます。これらすべてが最新のインターネット標準を用いて行われるので、プライバシーを守りながら、申し分のないパフォーマンスで快適に閲覧を続けられます。
https://support.apple.com/ja-jp/HT212614

1段階目のAppleのサーバはユーザの通信を受け入れ転送する,2段階目のcloudflare, akamai, fastlyのサーバは通信を暗号化し接続する,つまりAppleは通信された内容を把握しないし,依頼された企業は誰が通信したかを把握しないということになる…?
確かにコレなら「通信したユーザと通信した内容を1つの組織が両方を同時に把握する」ことは起こり得ないと言える…

というよりもSafariそのものがTor browserのような機能を持つことになる,どんな回線でもプロバイダの先で最初にicloudに接続し転送と暗号化を経てサイトにアクセスできる,それに加えてそこそこな速度も出るし(最大100Mbpsまでは期待できそう)

IPv6もOK

ちゃんと対応している,フルで置き換わる

QUICとは何か

datatracker.ietf.org tex2e.github.io www.cybertrust.co.jp

難しスギィ!
要するに

  • HTTP-over-QUICがHTTP/3で,
  • QUICはTCPTLS・HTTP/2の一部をUDP上で実装して,
  • IPアドレスが変わっても再接続が可能

ってことですか…?

VPNやTorとは何が違うのか

Safari系の画面でなければ素通りする

TwitterもNicovideoもPixivもJaneStyleもSFSafariViewControllerを使ってなさそうなのでダメっす,必ずSafariからやりましょう

位置情報が探られやすい

IPアドレス位置情報設定」で「おおよその位置情報を保持」にするとGoogleに都市レベルの位置情報をお出しされる
「国と時間帯を使用」でも国家レベルや時間帯レベルの位置情報が出てくる,つまり位置匿名性は高いとは言えない

実際TorやVPNやProxyならば出口ノードが海外におかれる場合も多く,それには劣ってしまう…と考えている(各リレーの中身がどんな風に構成されてるかにもよりそうだが,個人の集合体ではなく4社独占体制なので明らかに脆い)

開示請求するとどうなるの?

実はノーログだったりするのか,それとも素直に応じるタチなのか,Appleと3社の間でどんなやりとりが発生するのか,やってみなければわからない部分が非常に多く,興味深い
というか「プロバイダ←Apple←3社←Web管理者←申立人」みたいな4段構えになりかねない,「お互いにデータを照合しない」というPrivate relayの取り決めがどこまで通用してどこから通用しないのかが,そこが非常に気になる…

林檎ユーザだとバレる可能性がある

段々と出口サーバのIPが割れてくる可能性がある,というよりこの特定作業は有志が既に初めてそうなのが…
https://mask-api.icloud.com/egress-ip-ranges.csv
なんだこれは,たまげたなぁ…(14MBのcsv
既に全てのリレーの出口が掲示されているじゃないか…

中国とロシアで使えない

前者は公式発表だけど後者はユーザー報告,これは法律が違ったり圧力がかかったりしているせいなのですか?

今のところ確認されている不都合

日本国内限定

モバイルデータ通信による機能が阻害される

通信会社からユーザーを見れば,宛先が全てiCloudになる上に内容も秘匿されるので, 一切のコンテンツフィルタは動作しなくなる(本体内蔵型を除く)し, 特定サービスが対象の無料プランも検知できないから適用されない
iOS向けで出しているアプリであっても中身がSFSafariViewControllerで動いているものはアウトになるので, 契約してる携帯通信会社のページを確認しておこう

出口のIPアドレスがコロコロ変わる

特に匿名掲示板だとIDなどが切り変わるか(運がとても良ければ)被る可能性があり,思わぬ事故が発生するかもしれない
不都合なら火狐を使おう

やっぱり遅い

100Mbps以上の速度が欲しかったら使うな

締め出される可能性がある

非常に強い林檎アンチなコミュニティがあれば,IPアドレスから焼かれる可能性が考えられる
もしくは側から見ればプロキシとの違いが明確にわからない以上,トラフィック量だけを見てオートでブロックすれば, 間違ってフィルタされてしまう可能性だってある
後者は簡潔なホワイトリスト設定をAppleが出しているわけではなさそう,さっきのクソデカcsvで何とかするしかないみたい…

matlab / simulinkの円形のsum blockの完全な向きの変え方

まとめ

円形を選択した時にやりたい操作ですね

右回転

command(⌘) + R

左回転

shift(⇧) + command(⌘) + R

左右の(鏡像)反転

command(⌘) + I

上下の反転について

そんな操作はないですね
その代わりブロックパラメータ>メイン>符号リストをいじれば解決します

1入力1出力の場合

直線

f:id:Soluna_Eureka:20210825195149p:plain

上直角

f:id:Soluna_Eureka:20210825195203p:plain

下直角

f:id:Soluna_Eureka:20210825195227p:plain

2入力1出力の場合

上下直角

f:id:Soluna_Eureka:20210825195628p:plain

上直角空き

これがデフォルトでお出しされることが多い f:id:Soluna_Eureka:20210825195639p:plain

下直角空き

f:id:Soluna_Eureka:20210825195650p:plain

45°分け

かっこいいから好き f:id:Soluna_Eureka:20210825195703p:plain

感想

|は空白端子を意味するらしい
やってみたところこの記事にあるような単語を使っても検索結果には出てこなかったので共有しときます

xparseで条件分岐コマンドを生成 LaTeX

準備

\usepackage{xparse}
\usepackage{mleftright} %あったほうがいい

ドキュメント https://ctan.org/pkg/xparse

leftとrightを楽にしたい

\NewDocumentCommand\ld{}{\left.}
\NewDocumentCommand\rd{}{\right.}
\NewDocumentCommand\alc{}{\mleft\lparen}
\NewDocumentCommand\arc{}{\mright\rparen}

関数に変数を明示したい

\NewDocumentCommand\funct{m}{
    {#1}_{\alc t \arc}
}   %f(t)

積分記号の引数処理を楽にしたい

\NewDocumentCommand\intet{moo}{
    \IfValueTF{#2}{
        \IfValueTF{#3}{
            \int_{#2}^{#3}
        }{
            \int_{#2}
        }
    }{
        \int
    }
    {#1} {\mathrm{d}t}
}

\[
  \intet{\funct{f}}[T]
\]
  • #2があって#3があるなら $ \int $ の上下に引数を入れる
  • #2のみなら $ \int $ の下に引数を入れる
  • #2もないなら $ \int $ のみ

これだと $ \int_T f_{(t)} \mathrm{d}t $ が生成される

微分ry

\NewDocumentCommand\difft{mo}{
    \IfValueT{#2}{\ld}
    \frac{\mathrm{d}}{\mathrm{d}t} {#1}
    \IfValueT{#2}{\right\vert_{#2}}
}

\[
  \difft{x^2}[2] &= 4
\]
  • #2があるなら $ \vert $ と引数のセットをつける
  • #2がないならなし

これだと $ \left. \frac{\mathrm{d}}{\mathrm{d}t} x^2 \right|_2 = 4 $ が生成される

解説

関数

\IfValueTF{}{}\IfValueT{}\IfValueF{}

変数がある時〜!がtrueでない時〜!がfalse
TF{}{}trueの処理とfalseの処理を分けて書ける
それ以外はtruefalseのどちらかの時だけを書ける

\IfNotValueTF{}{}\IfNotValueT{}\IfNotValueF{}

その逆を往く

\IfBooleanValueTF{}{}\IfBooleanValueT{}\IfBooleanValueF{}

\BooleanTrue\BooleanFalseに対応する(後述)

\NewDocumentCommand

コマンド新設置

\NewDocumentCommand\新しい関数の名前{引数のオプション}{処理の中身}

\RenewDocumentCommand

コマンド上書き

\NewDocumentCommand\新しい関数の名前{引数のオプション}{処理の中身}

引数

関数側

最低限は以下のルールを守る

  • 前から#1#2#3…と呼ぶ
  • 念のために{#1}のように前後は{}で囲っておいたほうがいい
  • 定義の中で既に定義された別の関数を用いることができる
    • デフォルトの値にも設定できる
  • 変数に段落(\par動作)も含めたいならオプションの前に+をつけよう
  • コマンドの省略制御は割りと難しいので,使用頻度の高い順に番号を振るのが吉
  • mオプション
    • 入力がなければコンパイルエラー吐いて止まる
    • 要するに普通の引数と同じ
  • oオプション
    • 入力がなければ-NoValue-すなわち変数なしを返す
      • この時に変数を無理矢理に出力すると"-NoValue-"という文字列になる
      • でも"-NoValue-"を入力に渡すと-NoValue-は返されない
    • 入力があれば処理は続行する
  • O{デフォルトの値}オプション
    • 入力がなければデフォルトの値を採用する
    • 入力があれば入力を採用する
  • +bオプション
    • 環境設定的なbegin{}を作るのに重宝する
    • 引数の最後にこれを持ってくることで,以降の文章が全て入力として扱われる
  • sオプション
    • コマンドの後に*を入れてモードを切り替えるのに便利
    • *あれば\BooleanTrue,なければ\BooleanFalse
      • 引数設定の先頭にsがあるなら,このどっちかが必ず変数#1に入る
      • 別に先頭である必要はないが,慣習的に先頭の方がよさそう

本文側

だいたいこんな感じか

  • mオプション
    • \コマンド{引数}の形を取る
    • {}は入力ありと判定される
  • oオプション
    • \コマンド[引数]の形を取る
    • []は入力ありと判定される
  • Oオプション
    • \コマンド[引数]の形を取る
    • []は入力ありと判定される
  • sオプション
    • *の有無に依存する
    • 必ず\BooleanTrue\BooleanFalseのどちらかを返す
  • 省略制御は難しい
    • 例えば関数側で{s m m O{xyz} o o}と指定した時
      • []\BooleanFalse,-NoValue-,-NoValue-,"",-NoValue-,-NoValue-になる
      • {}[][]\BooleanFalse,"",-NoValue-,"","",-NoValue-,になる
    • 要するに引数の型をチェックして前に詰めていく上に「入力なし」を入力できないらしい

これを使う利点

割と楽に2つ以上の引数の省略が可能になる,使い分けが増えることで関数名が節約できる
(デフォルトでは自明に1個までしか省略できない)

意見・要望

積分記号あるじゃないですか,あれって

\[
  \left\int f(x) \right.
\]

的な感じでサイズを調整してくれるとマジで嬉しいんですよね
実際には! Missing delimiter (. inserted).つまり\intは区切り記号(括弧類)じゃないってエラー吐かれるんですが
でもだってほら,微分に使う$\vert$はleftrightできるし,$\int$も対応して欲しいなって

\begin{align} x = a+b \end{align}

TeX + VSCode + git on Mac 設定 メモ

前提

  • pdfファイルが欲しい
  • VSCode上で完結させる
    • CLIはなるべく使いたくない
  • latexmkで以下を一括で起爆する
    • uplatex
    • upbibtex
    • biber
    • upmendex
    • dvipdfmx
    • 1回のコマンド内で済む
  • gitでバージョン管理したい
    • 致命的なミスを回避したいので
  • macOSでスマン

下準備

gitインスコ

$ brew install git
$ echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.bashrc
$ source ~/.bashrc

homebrew入れてない人は適宜に導入してくれたほうが良い シェルのパスも通しておこうな(VSCodeから叩きたいので)

VSCodeインスコ

brew install --cask visual-studio-code

TeXShop on Macインスコ

ここから.pkgを落とす 適当に全部インスコする こっちはなんとなくだがhomebrewはおすすめしない TeXの管理にhomebrewを使うのは直感的ではなさそう

設定

GUIアプリケーション

TeXShop

実質的に不要(使わないので)

TeX Live Utility

適宜にライブラリをアプデしておこう ちなみに年度バージョン切り替えはこれじゃないと面倒

git

実質的に不要(GitHub使わないなら)

VSCode

latex関係

  • 拡張機能の欄を開いてLaTeXで検索
  • 以下をインストール
    • LaTeX Workshop
    • LaTeX Utilities
  • LaTeX Workshopの「拡張機能の設定」を開く
    • 右上の「設定(json)を開く」を選択
  • settings.jsonに以下をコピペ
{
"files.associations": {
    "*.sty": "latex",
    "*.cls": "latex"
},
"latex-workshop.latex.tools": [{
    "name": "latexmk",
    "command": "latexmk",
    "args": [
    "-e","$latex=q/uplatex %O -synctex=1 -interaction=nonstopmode -file-line-error %S/",
    "-e","$bibtex=q/upbibtex %O %B/",
    "-e","$biber=q/biber %O --bblencoding=utf8 -u -U --output_safechars %B/",
    "-e","$makeindex=q/upmendex %O -o %D %S/",
    "-e","$dvipdf=q/dvipdfmx %O -o %D %S -z 0/",
    "-norc",
    "-gg",
    "-pdfdvi",
    "%DOC%"
    ]
}],
"latex-workshop.view.pdf.viewer": "tab",
"latex-workshop.chktex.enabled": true,
"latex-workshop.chktex.delay": 0,
"latex-workshop.latex.autoBuild.run": "never",
"latex-workshop.latex.watch.delay": 0
}
  • 設定内容イメージ
    • ビルドにlatexmkを指定する
      • uplatexとdvipdfmxの組み合わせを想定
      • 最終ビルドでは"$dvipdf=q/dvipdfmx %O -o %D %S -z 0/""$dvipdf=q/dvipdfmx %O -o %D %S -z 3/"にしておくとファイル容量がむちゃくちゃ減少
      • これを雛形からやろなんて思たらクソダルいしコピペした方がええでマジ
    • セーブ時の自動ビルドを無効化する
    • pdfビューワをtabで開く
    • chktex(エラー検出ソフト,デフォルトでTeXLiveに入ってる)をオンにする
    • スタイルファイル(スクリプト置き場).styとクラスファイル(雛形設定置き場).clslatexファイルとして扱う

git関係

  • 拡張機能の欄を開いて@popular gitで検索
  • 以下をインストール
    • Git History
    • GitLens
    • Git Graph
    • Project Manager
    • gitignore
    • Git Blame
  • とりあえずこれででいいと思います…

使い方

  • ルートフォルダを開き.texファイル,.styファイル,.clsファイルを用意する
  • .bibファイルも必要なら
  • 画像とかを管理するフォルダは別に作った方がよさそう
  • .texファイル,.styファイル,.clsファイルを開いて編集する
  • 様式を満たした上で右上の再生ボタンを押す
    • latexmkなので1回で完全にビルドされる
    • 「View LaTeX PDF File」するとpdfが開く
      • でも動作が重いので別のビューワを持ってきた方がいいよ
    • エラー吐いたらコンパイラのログを読もう
      • ggれば高確率でTeX Wikiのどこかにぶち当たる
  • ワークスペースを保存しておく
    • 次回以降にVSCodeを立ち上げると勝手にセッティングしてくれるので
  • gitが用意できているなら
    • 初回は拡張機能からgit init
      • 画像とかあると時間がかかる,気を付けてね
    • 更新したらステージング,気が済んだらコミット
      • ブランチ,マージ,スタッシュ…などはそれ専用の解説記事を見てくれた方がいいです
  • 日本語の名前を使ってるフォルダかファイルがあると新規ファイルの追加でエラー吐く(ステージングできなくなる)
    • どうやらVSCode拡張機能gitのステージングは絶対パスを使ってスクリプトを打ってるらしく,そのどこかに日本語が入っているとgitがエラーを吐く
    • ターミナル開いてgit add -Aするとできる場合がある
      • こっちは相対パスで動いてるくさい(../a/b/cみたいなヤツか?)
      • ルートフォルダ含むプロジェクト内で日本語を使わなければこれで解決しそう
  • Git Graph
    • 更新履歴をツリーで見せてくれる
  • Git History
    • 更新履歴を細かく見せてくれる
  • GitLens
    • コミット当時のファイルと現在編集中のファイルを比較してくれる
  • Git Blame
    • GitLensのコンパクト版

初回でルートフォルダを選択し作業スペースとして開いたらgit系拡張機能が勝手に$HOMEをルートフォルダにして大量にファイルを漁り始める件について

  • 選択したフォルダの中に.gitがないと勝手にそうなっちゃうらしい
    • 自動git initはされないっぽい
    • settings.json"git.autoRepositoryDetection": "subFolders"を入れればこの動作自体は止まるっぽい
  • ちゃんとgit init.gitを作ってからブラウザやタブを再読み込みすると治るっぽい
    • シェルはちゃんと移動しているので安心してくれ

このやり方の利点

gitが有能

  • 自分の進捗が時刻や変更箇所と共に記録されるので,単純に進度管理にも便利だと思います
  • 取り返しのつかないミスをした時のリカバリーにもなると思います
  • パターンを分けながら制作する時にも役に立つと思います

VSCodeが有能

  • 圧倒的補完機能
  • 圧倒的検索機能
  • 圧倒的拡張機能
  • 圧倒的GUI

TeX on Mac 設定 メモ

前提

  • pdfファイルが欲しい
  • pdfTeXやdvips+ps2pdfは使わない
    • uplatexとdvipdfmxを使う
  • luascriptであるptex2pdfを使ってtexから.pdfまで一気にコンパイルする(TeXShopでデフォルトでできる)

下準備

TeXShop on Macインスコ

ここから.pkgを落とす
適当に全部インスコする

環境設定

タイプセット

デフォルトのコマンド

LaTeX

デフォルトのスクリプト

TeX + DVI

タイプセット後の動作

編集を続ける

ファイル保存時に

関連ファイルも保存する

Sync 方式

SyncTeX

内部設定

パス設定

コンパイルを起爆する実行ファイルがどこにあるかを指定する

TeX

/Library/TeX/texbin

Distiller (Ghostscript)

/usr/local/bin

homebrewで最新版を入れてるなら

/usr/local/Cellar/ghostscript/x.y.z/bin

でも良さそう

TeX+dvipdfmx

コンパイル時のコマンドを編集する

LaTeX

luascriptであるptex2pdfをどうやって起爆するかの設定

ptex2pdf -u -l -ot "-synctex=1 -file-line-error" -od "-z 0"
-u

upLaTex召喚オプションその1

-u

upLaTex召喚オプションその2

-ot

upLaTexコンパイラオプション召喚オプション
以下の""に文字列を突っ込む

-synctex=1

synctex有効化

-file-line-error

エラー位置をログに出力

-od

dvipdfmxオプション召喚オプション
以下の""に文字列を突っ込む

-z 0

コンテンツ(写真等)情報量の圧縮手順を切る,爆速コンパイルになる
ところで動画とかwebフレームの埋め込みとかもできるんだろうか…?

何が言いたいか

ptex2pdfのオプションついては https://texwiki.texjp.org/?ptex2pdf dvipdfmxのオプション(-od ""で呼び出せる)については https://texwiki.texjp.org/?dvipdfmx uplatexのオプションについての記事はなかったので
uplatex --helpして読むしかない

詳細

Distiller

Apple DistillerにするとバグるのでNG

ドキュメント

TeXファイル

先頭

\documentclass[a4paper,12pt,uplatex,fleqn,dvipdfmx]{jsreport}

uplatexdvipdfmxを指定しておいて,それに対応しているclsを読み込めばいいんじゃないですかね…

ipv6専用ddnsがフレッツ網で試供されていた

きっかけ

無料のDNSで遊びたかった
ipv4DNSは申請の敷居が高いし,ぶっちゃけv6プラスだと実質固定ipで面白くないし,なのであんまり乗り気になれなかった

と思ったらipv6専用ddnsが試供されていた,なるほどなぁ…

リンク

概要
HTTP(S)なAPI
NTPやpingで動かす場合
採用事例

HTTP(S) API

フレッツ網内かipv6インターネットから呼び出すのが前提
それ以外はddnsapi-v6ddnsapi-v4に書き換える
http://https://でもOK

dnsを新規設定

http://ddnsapi-v6.open.ad.jp/api/new/?[HostName]
  • 返却されるCSVの中にKeyが含まれる
  • KeyとHostNameが対応するので大事に取っておく

同一HostNameのアドレスを更新

 http://ddnsapi-v6.open.ad.jp/api/renew/?[Key]
  • このAPIを叩いたマシンのipv6アドレスが反映される
  • Keyは必須,HostNameは不要

Host Nameのみ更新

http://ddnsapi-v6.open.ad.jp/api/changehostname/?[Key]=[NewHostName]
  • これもKeyは必須
  • ipv6アドレスは反映されない

dnsそのものを削除

http://ddnsapi-v6.open.ad.jp/api/delete/?[Key]
  • これもKeyは必須

状況確認

http://ddnsapi-v6.open.ad.jp/api/status/?[Key]
  • [Key]を[HostName]にすると返却される情報の一部(機密なやつ)がhiddenされる

Keyを紛失する時のための対策

新規設定時に

http://ddnsapi-v6.open.ad.jp/api/new/?[HostName],[MailAddress]

しておくと紛失時に

http://ddnsapi-v6.open.ad.jp/api/recovery/?[MailAddress]

することでメアドに結びつけられたdnsとKeyの一覧が届くらしい ここでKeyの再設定がされることはない

利用例

まだやったことないけどブラウザから手で打ったら本当に動作したので間違いではなさそう

追記・ipアドレスの構成について

こっちがほんへ ipv6のアドレスの構成は,一般的なネットワークであればrfc4291によるところで

  • 上位,プレフィクス
    • 通常は64bitそのままサブネットプレフィックスとする
    • もしくは以下のように分割したものを合体
      • n bit,グローバルルーティングプレフィクス
      • 64-n bit,サブネット識別子
  • 下位,サフィクス
  • 48bitのMACアドレスをModified EUI-64で64bitに変換して申告し,そのままインターフェース識別子として使う
    • ↑実際はセキュリティ的な観点で現在は非推奨
  • もしくはDHCPv6サーバが64bitをランダムに生成して配布する

と規定されている.

NTTのNGN網でも通常のIPv6 IPoE接続であれば上記に従い,光電話を使用する場合は,NGNからONUにグローバルルーティングプレフィクスが48bitもしくは56bit(わからんけどここのp.56にある通りなら)で渡され,その下のHGWにはONUからサブネット識別子が16bitもしくは8bit(本体設定のDHCPv6サーバ払い出し状況を見るに下位4bitを割り当てて残りは0埋め?標準HGWに0を割り当て?)で渡され,残りの64bitを端末MACアドレスから生成(同上のp.26より,これはrfc3315によるもの)する.

ちなみに光電話を使用しない場合はONUのサフィクスが最初から64bitで降ってくるらしく,すなわちHGWの追加構成や自家構成ができない.

v6プラスオプションを申し込んだ上で光電話を使う場合も,ネイティブなIPv6インターネット機能を使うなら,上と同様のIPv6 IPoE接続を利用することになる.手元の環境を確認したところ,同じプレフィクスと異なるランダムなサフィクスを持つ2アドレスが振られている.

しかしIPv4接続をする場合は,ここのp.42にある通りに,v6プラスが採用しているMAP-Eの仕様であるrfc7597によるところで

  • 上位,プレフィクス→以下を結合して64bitに成形
    • VNE事業者が持つIPv6プレフィクス,上位n bits
    • MAP-Eで与えられるIPv4サフィクス,上位p bits
      • 要はIPv4アドレスの末尾がIPv6プレフィクスにくっつく
    • 同上のポート番号から得られるPSID,上位q bit
      • ポート番号の一部を用いて生成される
    • サブネットID
      • 64 - n - p - q > 0 の時に発生
      • 基本的に0埋め(HGWの複数使用は想定されていない?)
      • もし64 - n - p - q < 0なら後者の処理に影響する
      • これ,厳密にはプリフィクスとは呼ばないらしいです
  • 下位,サフィクス→以下を結合して64bitに成形
    • PSID(同上),下位q bit
    • MAP-Eで与えられるIPv4アドレス,下位32bit
    • 0パディング,下位から
      • 64 - q - 32 = rビットだけ埋まる
      • プレフィクス64 bitよりデカいとココが削られる

と規定されている.つまりインターフェースのMACアドレスが関与することなく4to6および6to4のNAPTが実現できており,これは一般ユーザには見えない(完全にIPv6で実装されたNTTのNGN網内でのみ使われるため,要するにHGWからVNEの間でのみ4to6 NAPTで得られるアドレスが使われる)ものとなる.

前述したが,DHCPv6サーバ払い出し状況の項目については,HGWがONUからセグメントを預ったルーターに関するもので,ONU・HGW一体型1つで利用する一般人は何も表示されないのが正解である.

そしてv6プラスオプションが有効な場合はMAP-Eの機能がHGW側に必要であるため,接続した時点で専用のソフトウェア(例の http://192.168.1.1:8888/enabler.ipv4/main )がオートに降ってくる.もし別のルータをONU・HGW一体型の配下に置いてMAP-Eの機能をさせたいのならば,v6プラスに対応するルータが先取してこのソフトウェアを入れないといけないし,ONUだけならもしかしたら沢山HGWもどきを設置できる…のかも知れない.固定IP取ったv6プラスでも契約しなきゃメリットなさそうだけど.

環境設定のネットワークのTCP/IPIPv6の設定のルーター欄に恐らくfe80::で始まるアドレスがあるが,それは192.168.1.1と同じ意味を持ちHGWにアクセスできるローカルIPv6アドレスである.

参考

ちなみにNURO光やその他の非NGNipv6対応の光回線はさらに仕様が違うらしい,なのでここでは扱わない

ipv6アドレスとVNEの関係

これでプレフィクスを取得できるらしいな?

$ curl -H 'Host:route-info.flets-east.jp:49881' -H 'Accept:*/*' -H 'Connection:close' --http1.1 http://route-info.flets-east.jp:49881/v6/route-info
0000,2020/09/17 09:23:31
1111,2404:01a8:7e00:0000:0000:0000:0000:0000/40 --NTT東日本
1211,2404:01a8:0000:0000:0000:0000:0000:0000/32 --NTT東日本
1212,2001:0c90:0000:0000:0000:0000:0000:0000/33 --NTT東日本
1311,2408:0210:0000:0000:0000:0000:0000:0000/30 --NTT東日本
1411,2400:2410:0000:0000:0000:0000:0000:0000/30 --BBIX-IPv6
1412,2409:0010:0000:0000:0000:0000:0000:0000/30 --MF-Transix-E-1
1413,240b:0010:0000:0000:0000:0000:0000:0000/30 --JPNE
1414,2404:7a80:0000:0000:0000:0000:0000:0000/30 --BIGLOBE
1415,2405:6580:0000:0000:0000:0000:0000:0000/30 --朝日ネット
1416,2400:4050:0000:0000:0000:0000:0000:0000/30 --OCN
1417,2401:4d40:0000:0000:0000:0000:0000:0000/30 --フリービット
1418,2001:0f70:0000:0000:0000:0000:0000:0000/30 --アルテリア・ネットワーク
1419,240b:c0c4:0000:0000:0000:0000:0000:0000/30 --楽天モバイル

4連続のNTT東日本を除けば現在は9社でVNEをやっているらしい,確認してみよう https://www.ntt-east.co.jp/info-st/ipoe_menu/

参考